Skip to main content
Version: 3.x

Events & Observers (reacting to what CBX does)

Version
3.x
Updated
View markdown

How to run your own code when something happens in CBX — an order is placed, a record is stored, a tax rate is computed, a customer registers — without modifying core. This is the observer mechanism: you write a class with methods named after events, register it via a small DB record, and the framework calls your methods alongside the built-in ones.

This is the safest, most upgrade-resilient way to inject behavior: it is additive (you add to a dispatch list; you don't replace any class), so it never drifts out of sync the way a class override does. Prefer it over overriding controllers/models whenever "react to X" or "augment X" describes your need.

Read com_configbox_customization_overview.md first. 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. How observers work

The dispatcher is KenedoObserver (external/kenedo/classes/KenedoObserver.php). An observer is a class whose method names are event names. When code fires an event, the dispatcher calls that method on every registered observer that has it (triggerEvent(), :54-89):

foreach (self::$observers as &$observer) {
if (method_exists($observer, $eventName)) {
$returns[get_class($observer)] = call_user_func_array(array($observer, $eventName), $parameters);
}
}

So you don't subscribe to specific events — you just implement the methods you care about and ignore the rest. Registration is lazy: observers are registered the first time any event fires (:56-62), in this order:

  1. Custom observers with after_system = 0 (registerCustomObservers(false))
  2. All built-in observers (observers/)
  3. Custom observers with after_system = 1 (registerCustomObservers(true))

That ordering is the lever for "run before vs. after the core observers" (§4).


2. Writing an observer

An observer is a plain class named Observer<Something>, with one method per event you want to handle. The class file lives in data/customization/custom_observers/, and the class name is derived from the file name: registerObservers()/registerCustomObservers() build it as 'Observer'.<basename-without-extension> (:22, :41). So a file MyShipping.php must declare class ObserverMyShipping.

// data/customization/custom_observers/MyShipping.php
<?php
defined('CB_VALID_ENTRY') or die();

class ObserverMyShipping {

// React: send a webhook when an order reaches a paid status.
function onConfigBoxSetStatus($orderId, $status) {
if ($status != ConfigboxOrderHelper::getStatusCodeForType('paid')) {
return;
}
// … your side-effect (queue a webhook, write a log row, etc.)
}
}

The method signature must match the event's (§5) — including the & for by-reference parameters, or you won't see the caller's mutations (§3).


3. The two event styles — react, augment, compute

Events come in three flavors. Knowing which one you're handling tells you how to write the method.

3.1 React (fire-and-forget)

The dispatcher ignores your return value; you just do a side-effect. Example call site (KenedoModel.php:374):

KenedoObserver::triggerEvent('onAfterStoreRecord', array($this->getModelName(), $data));
function onAfterStoreRecord($modelName, $data) {
if ($modelName != 'adminorders') return; // scope to the model you care about
// … react …
}

3.2 Augment (by-reference)

The caller passes a value by reference and reads it back after the event. Your method mutates the argument in place — it must declare the parameter with &. Example call site (onConfigBoxAddToCart, fired with array(&$cartDetails)):

KenedoObserver::triggerEvent('onConfigBoxAddToCart', array(&$cartDetails));
function onConfigBoxAddToCart(&$cartDetails) { // ← the & is REQUIRED to affect the caller
$cartDetails->myFlag = true;
}

Forgetting the & is the #1 observer bug. Without it you get a copy; your changes vanish. Match the built-in signature exactly (onConfigBoxAddToCart(&$cartDetails), onConfigBoxGetStatus(&$cartDetails), onConfigboxGetTaxRate(&$taxRate, $taxRateId, $userId = NULL)).

3.3 Compute (return value, last-wins)

When the caller passes $returnLast = true to triggerEvent(), it uses the last registered observer's return value (:78-85). Example call site (onConfigboxGetPaymentOptions):

$paymentOptions = KenedoObserver::triggerEvent('onConfigboxGetPaymentOptions', array($cartDetails->userInfo, $baseTotal), true);
function onConfigboxGetPaymentOptions($customerData, $baseTotal) {
return $myPaymentOptions; // your return can become THE result — see the ordering caveat
}

"Last wins" + registration order interact. With $returnLast, the value used is from whichever observer ran last. Built-ins run in the middle; your after_system = 1 observers run after them (§1), so an after_system = 1 observer's return overrides the built-in's. If you need to supersede a built-in computed result, register after_system = 1; if you need to provide a fallback the built-in can override, register after_system = 0. (For non-returnLast events all returns are collected but the caller typically ignores them.)


4. Registering your observer (the DB connector)

Unlike most customization mechanisms, dropping the file in place is not enough — custom observers are gated by a connector record in the #__configbox_connectors table. registerCustomObservers() only loads files named by a published connector row, and uses its after_system flag for ordering (KenedoObserver.php:29-48); the rows are read by ConfigboxCacheHelper::getCustomConnectors() (helpers/cache.php:1639-1660):

SELECT * FROM `#__configbox_connectors` WHERE `published` = '1' ORDER BY `ordering`

The table (helpers/updates/2.6.0.php:1312-1320):

ColumnMeaning
namea label for the admin list
filethe file name in data/customization/custom_observers/ (e.g. MyShipping.php) → class ObserverMyShipping
after_system1 = register after built-ins (default); 0 = before built-ins (§1, §3.3)
published1 to enable (the query filters on this)
orderingsort order among connectors

You add the row through the admin UI — the Connectors screen (controllers/adminconnectors.php, models/adminconnectors.php, views/adminconnectors) — or via a customization migration that inserts the row. Either way:

Two-part deployment. (1) the file under custom_observers/, and (2) a published connector row pointing at it. Miss the row and your observer silently never runs. The connector list is cached (getCustomConnectors() memoizes + persists), so after inserting a row directly in the DB, clear the CBX cache.

Delivering the row via migration (so it ships with your code) looks like:

// data/customization/updates/<version>.php
defined('CB_VALID_ENTRY') or die();
$db = KenedoPlatform::getDb();
$exists = $db->getObject("SELECT id FROM `#__configbox_connectors` WHERE `file` = ".$db->getQuoted('MyShipping.php'));
if (!$exists) {
$db->query("INSERT INTO `#__configbox_connectors` (`name`,`ordering`,`published`,`after_system`,`file`)
VALUES (".$db->getQuoted('My shipping webhook').", 100, 1, 1, ".$db->getQuoted('MyShipping.php').")");
}

(Follow the existing escaping idioms — getQuoted() quotes values; see technical/com_configbox_migrations.md.)


5. The event catalog

These are the events fired via KenedoObserver::triggerEvent() in component 3.4.1. Style: R = react, A = augment (by-reference — note the &), C = compute (returnLast). Match the signature exactly.

Lifecycle / system

EventSignatureStyleFired when
onConfigboxInitialized()RCBX finished booting (observers/System.php:6 loads the system overrides here).
onBeforeRender()Rbefore output rendering.
onAfterStoreRecord($modelName, $data)Rany model record stored (KenedoModel.php:374).
onAfterCopyRecord($modelName, $newData)Rany model record copied (KenedoModel.php:603).
onAfterDeleteRecord($modelName, $id)Rany model record deleted (KenedoModel.php:1457).

Cart / checkout / order

EventSignatureStylePurpose
onConfigBoxAddToCart(&$cartDetails)Aaugment a configuration as it's added to the cart.
onConfigBoxCheckout(…)R/Aat checkout.
onConfigBoxCartProcessingBeforeDiscounts(…)Aduring cart totals, before discounts.
onConfigBoxCartProcessingAfterPositions(…)Aduring cart totals, after positions.
onConfigBoxLoadOrderRecordBeforeDiscounts(…)Aloading an order, before discounts.
onConfigBoxAfterLoadOrderRecord(…)Aafter an order record is loaded.
onConfigBoxGetCbOrderId(&$cartDetails)Aresolve/assign the CB order id.
onConfigBoxUpdateUserInfo(…)Auser info on a cart/order changes.

Order status & permissions

EventSignatureStylePurpose
onConfigBoxSetStatus($orderId, $status)Ran order's status was set — the main "order happened" hook (built-ins use it for GA, notifications, tracking).
onConfigBoxGetStatus(&$cartDetails)Aaugment status info on load.
onConfigBoxGetStatusCodes()Ccontribute the set of status codes.
onConfigBoxGetStatusCodeForType($type)Cmap a status type ('paid', 'checked out', …) to a code.
onConfigBoxGetActionPermission($action, $cartDetails)Callow/deny an action on an order.

Pricing / tax / shipping / payment

EventSignatureStylePurpose
onConfigboxGetTaxRate(&$taxRate, $taxRateId, $userId = NULL)Aoverride the tax rate.
onConfigboxGetDeliveryOptions($cartId, $customerData, $weight, $maxDimensions, $cheapestOnly = false)Cprovide shipping options.
onConfigboxGetPaymentOptions($customerData, $baseTotal)Cprovide payment options.

Customer / auth

EventSignatureStylePurpose
onCustomerRegistration($customerData)Ra customer registered.
onUserLogin(…)Rlogin succeeded.
onUserLoginFailure(…)Rlogin failed.
onContentPrepare(…)Acontent-preparation hook (Joomla-style).

Verify the exact signature against the trigger site for your version before relying on it — params evolve. Grep the codebase: grep -rn "triggerEvent('<EventName>'" . shows the call site (and whether a param is passed by &reference and whether true is passed for returnLast). The catalog above is a map; the call site is the source of truth.

⚠️ Not events: onOrderPlaced, onPaymentReceived, onQuotationRequested, onOrderAddressSupplied are internal convenience methods on ObserverTracking (called from its own onConfigBoxSetStatus), not dispatched through triggerEvent. Implementing them on your observer does nothing. To react to those moments, handle onConfigBoxSetStatus and branch on the status code.


6. Worked example — webhook on a placed order

React when an order is placed (status set to "checked out"), additive and side-effect-only.

// data/customization/custom_observers/OrderWebhook.php (→ class ObserverOrderWebhook)
<?php
defined('CB_VALID_ENTRY') or die();

class ObserverOrderWebhook {

function onConfigBoxSetStatus($orderId, $status) {

// Only fire for the "checked out" status.
$checkedOut = KenedoObserver::triggerEvent('onConfigBoxGetStatusCodeForType', array('checked out'), true);
if ($status != $checkedOut) {
return;
}

// Load order data and post it somewhere. Keep it fast / fail-soft — don't break checkout.
try {
$order = ConfigboxOrderHelper::getOrderRecord($orderId);
// … send $order to your endpoint …
}
catch (Exception $e) {
KLog::log('OrderWebhook failed for order '.$orderId.': '.$e->getMessage(), 'error');
}
}
}

Then register it (admin Connectors screen, or a migration):

INSERT INTO `#__configbox_connectors` (`name`,`ordering`,`published`,`after_system`,`file`)
VALUES ('Order webhook', 100, 1, 1, 'OrderWebhook.php');

Clear the cache, place a test order, confirm the webhook fired.


7. Deployment checklist

data/customization/
custom_observers/
<File>.php ← class Observer<File> with methods named after events
updates/
<version>.php ← (optional) migration inserting the #__configbox_connectors row
  1. Write the observer data/customization/custom_observers/<File>.php → class Observer<File>, implementing only the event methods you need.
  2. Match the event signature exactly, including & on by-reference params (§3.2/§5).
  3. Register a connector row (#__configbox_connectors) — admin Connectors screen or a migration. Set published = 1 and choose after_system (1 = after built-ins, 0 = before) per §3.3/§4.
  4. Clear the CBX cache (the connector list is cached).
  5. Keep handlers fast and fail-soft — observers run inside live flows (checkout, save). Catch your own exceptions; don't let a webhook break an order.
  6. Verify manually — trigger the real event and confirm your method ran (and, for augment events, that your mutation stuck). CBX has zero automated tests.

8. Conventions & gotchas

  • Two-part deployment: the file and a published connector row. No row → never runs (§4).
  • Observer<File> naming — class name = Observer + the file's basename (:22,:41).
  • & on by-reference params or your augmentation is lost (§3.2). Copy the built-in signature.
  • after_system controls ordering and therefore "last wins" for returnLast compute events (§3.3).
  • The connector list is cached — clear cache after DB changes.
  • onConfigBoxSetStatus is the main order hook; the onOrderPlaced-style names are not events (§5).
  • Fail-soft and fast — observers run in checkout/save paths; an unhandled exception can break the flow.
  • Verify the signature at the call site (grep triggerEvent('<Event>') — params and returnLast vary by version.
  • Escape/quote in migrations and DB writesgetQuoted(); follow neighboring code.

See also

  • com_configbox_customization_overview.md — the extension-point map (observers are the registration mechanism).
  • com_configbox_overriding_controllers_and_models.md — when you must change behavior rather than react to it; explains why observers/property-injection are preferred over class replacement.
  • com_configbox_payment_connectors.md — payment integrations (a related connector mechanism).
  • technical/com_configbox_migrations.md — delivering the #__configbox_connectors row via migration.
  • external/kenedo/classes/KenedoObserver.php — the dispatcher: registration order (:29-62), dispatch + returnLast (:54-89).
  • observers/ — built-in observers (Orders, Notifications, GoogleAnalytics, Tracking, …) as worked references.