Skip to main content

Architecture & conventions

Audience: developers & AI agents · Scope: how the codebase is laid out and the rules for changing it safely · Last reviewed: 2026-07-12

TL;DR: This is a Joomla 5 site whose main component is the ConfigBox configurator (built on the Kenedo framework). Beta-Calco's own code lives almost entirely under docroot/components/com_configbox/data/customization/. Treat Joomla core, Kenedo, and vendor libraries as read-only third-party code — extend/override them from the customization tree rather than editing them in place.

The golden rule

Anything outside data/customization/ is replaced when Joomla, ConfigBox, or a vendor library is updated. So:

  • Read core/Kenedo/vendor code freely to understand behavior.
  • Don't modify it. To change core behavior, add code under data/customization/ and use an override mechanism (below).

Layout of com_configbox

docroot/components/com_configbox/
├── configbox.php ← front-end entry point (request dispatch)
├── router.php ← Joomla SEF router (build/parse clean URLs)
├── controllers/ models/ views/ ← ConfigBox core MVC (third-party)
├── external/kenedo/ ← the Kenedo framework (third-party)
│ ├── classes/ ← KenedoController, KenedoModel, KenedoView, KenedoDatabase, …
│ ├── platforms/joomla/ ← Joomla glue (KenedoPlatform::p() implementation)
│ └── helpers/ ← KenedoRouterHelper, init, …
└── data/customization/ ← ★ Beta-Calco's code ★ (edit here)

Inside data/customization/

SubdirPurpose
controllers/ models/ views/Custom MVC. Custom pages/features (usually bc-prefixed), e.g. bcquotelandingpage, bcmyquotes.
system_overrides/Replace/extend core PHP classes (esp. data objects like BcQuote). Wired via KenedoAutoload::registerClass(...).
language_overrides/Override translation strings.
model_property_customization/ properties/Add/adjust model fields, and add custom property types. A file properties/<type>.php with class KenedoProperty<Type> is auto-discovered (customization wins over core) and used by 'type' => '<type>' in a model's property defs — e.g. the read-only bcimported / bcimportedjoin / bcimportedboolean types that render import-owned fields as static, non-saving text. A custom type that extends a non-base built-in (…String/…Join/…Boolean) must require that base file first — property-type files load on demand, so the parent may not be loaded yet.
rule_condition_types/Custom configurator rule/condition logic.
updates/Numbered DB/schema migrations (auto-run). See migrations.md.
assets/Custom CSS/JS (and minified builds), served via getDirCustomizationAssets(). CSS and JS behave differently here — see Minified custom assets.
templates/Custom view/email templates.
libs/Third-party PHP libraries used by custom code (Composer vendor/).

Minified custom assets: CSS yes, AMD JS no

Worth knowing before you spend time regenerating a .min.js that nothing loads.

  • CSS is served minified. KenedoView rewrites foo.cssfoo.min.css when one exists, so editing a custom stylesheet has no visible effect until you regenerate the .min.css (csso foo.css -o foo.min.css).
  • Custom AMD modules are not. assets/main.js appends .min to the RequireJS path config only when useMinifiedJs is on — and it explicitly skips the configbox and configbox/custom entries, because those map to directories rather than to single files. A module under configbox/custom/… therefore always resolves to the plain .js; its .min.js sibling is never requested. Verified from the network log: the page loads …/customization/assets/javascript/adminbcbranchcleanup.js?version=…, never the .min.js.

Either way, bump the release number in system_overrides/releasenumber.php after touching a custom asset. It is the <appVersion>-<releaseNumber> cache-buster RequireJS appends to every module URL; without a bump, browsers keep serving the cached old file.

Naming conventions

  • Classes: Configbox + Controller/Model/View + Ucfirst(name) — e.g. view bcquotelandingpageConfigboxControllerBcquotelandingpage, ConfigboxModelBcquotelandingpage, ConfigboxViewBcquotelandingpage. (See configbox.phpKenedoController::getControllerClass().)
  • Custom features are typically prefixed bc (Beta-Calco) to distinguish them from core ConfigBox.
  • A view lives in views/<name>/view.html.php with templates under views/<name>/tmpl/.
  • Every PHP file starts with defined('CB_VALID_ENTRY') or die();.

Request lifecycle (front end)

index.php?option=com_configbox&view=<name>&... (or controller=<name>&task=<task>)


configbox.php
│ reads option / controller / view / task (task defaults to 'display')
│ resolves ConfigboxController<Name> and calls ->execute($task)

ConfigboxController<Name>::display() → getDefaultView()->... → rendered output
  • view=<name> and controller=<name> both resolve to ConfigboxController<Name>; the difference matters for SEF (only view= URLs get clean paths). See sef-links.md.
  • AJAX/tasks use controller=<name>&task=<task> and usually return JSON (ConfigboxJsonResponse, with KenedoPlatform::p()->setDocumentMimeType('application/json')).
  • SEF building/parsing is centralized in router.php (ConfigboxBuildRoute / ConfigboxParseRoute), which delegates to each controller's SEF hooks.

Override mechanisms (instead of editing core)

  • Data objects / classes: add a file in system_overrides/ that registers the override via KenedoAutoload::registerClass('<ClassName>', KenedoPlatform::p()->getDirCustomization().'/system_overrides/<File>.php') and defines a class extending the original (e.g. class BcQuote extends DataObjectAbstract).
  • Behavior on every request: observers/ (e.g. observers/System.php runs pending migrations on each request via ConfigboxUpdateHelper::applyUpdates()).
  • Strings: language_overrides/. Schema/data: updates/ migrations.

system_overrides/ load order. Files here are require_once'd on every request during init (onConfigboxInitialized), sorted alphabetically with _-prefixed files first. Use them for static helper classes, global function overrides, and custom data-object classes — not MVC artifacts (those go in controllers/ models/ views/).

Override resolution is NOT uniform — the main footgun

"Customization overrides core" is only half true — resolution order depends on the artifact type:

TypeOrder checkedEffect
Controllerscustomization first, then coreA same-named custom file does override core.
Modelscore first, then customizationA same-named custom file is ignored if a core model exists. The customization slot is for new models only.
Viewscore first, then customizationSame as models — a custom file is only used when no core view of that name exists.

(Verified in external/kenedo/classes/KenedoController.php, KenedoModel.php, KenedoView.php.)

Consequences:

  • To change a controller's behaviour, drop a same-named file in customization/controllers/.
  • You cannot override a core model or view by adding a same-named file — it's silently ignored. To alter a core model's data/behaviour, use model_property_customization/<modelname>.php (its customPropertyDefinitions<Modelname>() array is merged into the core model) or a system_overrides/ file. The custom bc*/adminbc* models & views here are all new artifacts, not overrides.

Cross-cutting integrations

The custom models talk to several external systems (e.g. Pipedrive CRM sync, Google Sheets, AWS metrics). Scheduled/batch work for these lives in docroot/cli/cb_*.php (run as php docroot/cli/cb_<name>.php) — Pipedrive sync & cold-quote prompts, Google-Sheet schedule/price/BOM exports, Infor ERP reads (BcHelper::getInforDb()), BigQuery, GA4, and AWS CloudWatch metrics. The integrations with their own docs are Pipedrive and tracking; the rest aren't fully documented yet — see the relevant models/bc*.php files and any draft notes under notes/, and promote what you learn into a dedicated doc (see README.md).

Platform abstraction

Kenedo can run on Joomla, WordPress, Magento, or standalone. Code reaches the platform through KenedoPlatform::p() (routing, mailer, document, dirs, language) and KenedoPlatform::getDb() (database). Prefer these abstractions over Joomla APIs in custom code, so behavior stays consistent with the framework.