Skip to main content
Version: 3.x

Overriding Controllers & Models (changing behavior)

Version
3.x
Updated
View markdown

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.

KindLoaderChecks firstResult
ControllerKenedoController::getController() (external/kenedo/classes/KenedoController.php:101-108)coreCore wins → customization controllers/ is for new controllers only; it cannot shadow a core controller.
ModelKenedoModel::getModel() (external/kenedo/classes/KenedoModel.php:168-176)coreCore wins → customization models/ is for new models only; it cannot shadow a core model.
View classKenedoView::getView() (KenedoView.php:202-207)coreCore winsnew views only.

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

// 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:

KindClass nameFile
ControllerConfigboxController<Name>controllers/<name>.php (<name> = lowercase of the part after Controller)
ModelConfigboxModel<Name>models/<name>.php (<name> = lowercase of the part after Model)

Examples: ConfigboxControllerCartcontrollers/cart.php; ConfigboxModelProductsmodels/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:

// 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): ConfigboxControllerMyexportcontrollers/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:

// 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:

    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 onesystem_overrides/ (§3.2 / §5).
  4. Model, new → customization models/ (§4).
  5. Model, behavior changesystem_overrides/ (§5). Model, data/field changemodel_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.mdreact 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.mdKenedoController 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.