Events & Observers (reacting to what CBX does)
- Version
- 3.x
- Updated
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:
- Custom observers with
after_system = 0(registerCustomObservers(false)) - All built-in observers (
observers/) - 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; yourafter_system = 1observers run after them (§1), so anafter_system = 1observer's return overrides the built-in's. If you need to supersede a built-in computed result, registerafter_system = 1; if you need to provide a fallback the built-in can override, registerafter_system = 0. (For non-returnLastevents 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):
| Column | Meaning |
|---|---|
name | a label for the admin list |
file | the file name in data/customization/custom_observers/ (e.g. MyShipping.php) → class ObserverMyShipping |
after_system | 1 = register after built-ins (default); 0 = before built-ins (§1, §3.3) |
published | 1 to enable (the query filters on this) |
ordering | sort 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
| Event | Signature | Style | Fired when |
|---|---|---|---|
onConfigboxInitialized | () | R | CBX finished booting (observers/System.php:6 loads the system overrides here). |
onBeforeRender | () | R | before output rendering. |
onAfterStoreRecord | ($modelName, $data) | R | any model record stored (KenedoModel.php:374). |
onAfterCopyRecord | ($modelName, $newData) | R | any model record copied (KenedoModel.php:603). |
onAfterDeleteRecord | ($modelName, $id) | R | any model record deleted (KenedoModel.php:1457). |
Cart / checkout / order
| Event | Signature | Style | Purpose |
|---|---|---|---|
onConfigBoxAddToCart | (&$cartDetails) | A | augment a configuration as it's added to the cart. |
onConfigBoxCheckout | (…) | R/A | at checkout. |
onConfigBoxCartProcessingBeforeDiscounts | (…) | A | during cart totals, before discounts. |
onConfigBoxCartProcessingAfterPositions | (…) | A | during cart totals, after positions. |
onConfigBoxLoadOrderRecordBeforeDiscounts | (…) | A | loading an order, before discounts. |
onConfigBoxAfterLoadOrderRecord | (…) | A | after an order record is loaded. |
onConfigBoxGetCbOrderId | (&$cartDetails) | A | resolve/assign the CB order id. |
onConfigBoxUpdateUserInfo | (…) | A | user info on a cart/order changes. |
Order status & permissions
| Event | Signature | Style | Purpose |
|---|---|---|---|
onConfigBoxSetStatus | ($orderId, $status) | R | an order's status was set — the main "order happened" hook (built-ins use it for GA, notifications, tracking). |
onConfigBoxGetStatus | (&$cartDetails) | A | augment status info on load. |
onConfigBoxGetStatusCodes | () | C | contribute the set of status codes. |
onConfigBoxGetStatusCodeForType | ($type) | C | map a status type ('paid', 'checked out', …) to a code. |
onConfigBoxGetActionPermission | ($action, $cartDetails) | C | allow/deny an action on an order. |
Pricing / tax / shipping / payment
| Event | Signature | Style | Purpose |
|---|---|---|---|
onConfigboxGetTaxRate | (&$taxRate, $taxRateId, $userId = NULL) | A | override the tax rate. |
onConfigboxGetDeliveryOptions | ($cartId, $customerData, $weight, $maxDimensions, $cheapestOnly = false) | C | provide shipping options. |
onConfigboxGetPaymentOptions | ($customerData, $baseTotal) | C | provide payment options. |
Customer / auth
| Event | Signature | Style | Purpose |
|---|---|---|---|
onCustomerRegistration | ($customerData) | R | a customer registered. |
onUserLogin | (…) | R | login succeeded. |
onUserLoginFailure | (…) | R | login failed. |
onContentPrepare | (…) | A | content-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 passedby &referenceand whethertrueis passed forreturnLast). The catalog above is a map; the call site is the source of truth.
⚠️ Not events:
onOrderPlaced,onPaymentReceived,onQuotationRequested,onOrderAddressSuppliedare internal convenience methods onObserverTracking(called from its ownonConfigBoxSetStatus), not dispatched throughtriggerEvent. Implementing them on your observer does nothing. To react to those moments, handleonConfigBoxSetStatusand 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
- Write the observer
data/customization/custom_observers/<File>.php→ classObserver<File>, implementing only the event methods you need. - Match the event signature exactly, including
&on by-reference params (§3.2/§5). - Register a connector row (
#__configbox_connectors) — admin Connectors screen or a migration. Setpublished = 1and chooseafter_system(1 = after built-ins, 0 = before) per §3.3/§4. - Clear the CBX cache (the connector list is cached).
- 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.
- 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_systemcontrols ordering and therefore "last wins" forreturnLastcompute events (§3.3).- The connector list is cached — clear cache after DB changes.
onConfigBoxSetStatusis the main order hook; theonOrderPlaced-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 andreturnLastvary by version. - Escape/quote in migrations and DB writes —
getQuoted(); 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_connectorsrow 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.