What's new for developers (CBX 4)
- Version
- 3.x
- Updated
New and changed capabilities that matter when you build on, extend, or operate CBX 4 as a developer. For how to fix customization code broken by these, see the breaking-changes log and the playbooks; this page is the "what you now have" companion.
This page is about the jump to 4.0. For what has been built since — the HTTP API, the MCP server, the CLI suite, generated record classes and schemas, prepared statements — see Latest features, and the HTTP API, the MCP server and platform & extensibility in particular. That set is cut by surface rather than by audience, so it is a different read, not a continuation of this one.
Simpler answer model
- One
#__configbox_answerstable replaces the oldoptions+xref_element_optionpair. Every answer belongs to exactly one question (clean 1:n) and is a self-contained row: its price/sku/weight/ images/custom fields and its title/description strings all hang off the answer id. No more "global option" reuse, no more merge-at-cache-time, no more two-ids-per-answer. - Why you'll like it: answer queries are single-table; the answer id is the one identity to reason about; the admin has a single Answers editor instead of a global-options list + an assignment editor.
- Compat shims mean cleanly-written customizations keep working (legacy
getModel()aliases, legacymodel_property_customizationloading).
Read more: the answer collapse, in depth — the old and new shapes, what the migration does to your data, and every shim · looking up assignments · building a product, from the top
Rules and formulas read fields the question type declares
- A question type declares what a rule or a formula can read off it —
getRuleFields(),getCalcFields()andgetFieldValue($key, $selection, $answerId, $selections)onConfigboxQuestion<Type>. The one generic question condition and term render the chips under the question, validate on save, evaluate, remap on copy and transfer, and describe the fields to the AI authoring tools. A composite type (dimensions:w,h,area) declares its members as fields; a per-answer quantity is a field declared once per answer. No more condition class, term class and editor sub-view per question type — classes you still ship keep working under "Other conditions". - One stored vocabulary:
{"type":"QuestionProperty","questionId":42,"field":"selected","operator":"==","value":"57"}.selectedAnswer.id,selection,fieldPathand the pre-3.6datawrapper are gone from the data (migrations 3.8.26 and 3.8.29) and tolerated on read. - Comparison per field kind (
ConfigboxQuestionField::matches()): numbers as numbers, dates by day, choices and texts as strings; an unanswered question isnull, never0, so an ordering on an empty number field is false and an empty condition value means "not answered". - The question type is fixed once saved, except for a change to a type the stored type declares
compatible (
getCompatibleTypes()): the answer-taking types among each other, the text types among each other. The check is on the model, so every writer is refused alike. - Price helpers take the set they price:
ConfigboxPrices::getQuestionPrice($id, array $selections, …)and the answer, recurring and weight helpers alike. A rule on a preview or a term in a formula never prices the live session by accident again; the deprecatedgetElement*aliases keep the live default. - The installer removes what the version no longer ships (
helpers/retired_files.txt), so a directory scan cannot resurrect a retired condition type after an upgrade.
Read more: the breaking-changes log, entries 2026-09-06 and 2026-09-07 ·
custom question types, §2.6 "Rules and formulas — the type declares its fields" and §2.9 "Two more things a type may declare" (getCompatibleTypes()) · the rule engine §4.3 ·
the calculation engine
Reactive configurator client state (a Redux-style store)
- The configurator page's client state now lives in a small Redux-style store — a new AMD module
configbox/store(assets/javascript/store.js) — instead of a write-once jQuery blob on#configurator-data. It's a single immutable state tree withdispatch(action)/getState()/subscribe(listener)/ selectors, normalisedentities.questions(by-id) + a per-questionselectionsmap, and oneMERGE_SERVER_RESPONSEaction that folds each configurator server response in atomically. The state is maintained through interactions (a selection round-trip updatesmissingProductSelections,pricing, the recorded selection, …) — the old blob drifted out of sync. - Backward-compatible. The classic
configurator.getConfiguratorData()/setConfiguratorDataItem()/replaceConfiguratorData()/getQuestionPropValue()/questionHasProperty()accessors are now thin shims over the store, and the store mirrors a flat snapshot back into#configurator-data's jQuery.data('json')after every change. New code can useconfigurator.getStore()orcbrequire(['configbox/store'], …)directly. - Gotcha: the DOM
#configurator-datadata-jsonattribute is only the frozen initial seed — read the store (or the accessor) for current state. And jQuery is AMD-scoped (cbj), not a global.
Read more: the configurator client store — the state tree, every action and selector, the legacy bridge, and how customization JS should read and watch it · the configurator loop, server and client · the dated store entry in the breaking-changes log
A real CLI command suite (Joomla 5 console)
Run via php cli/joomla.php configbox:<cmd>, grouped under the configbox namespace:
configbox:cache:clear— clear all CB caches.configbox:migrate— apply outstanding migration scripts (--clear-failed-flagto recover a frozen update;--statusto report what is pending without applying anything;-vfor the full failure diagnostics — exception chain, stack trace, migration-log tail).configbox:migrate:unblock— lift the freeze a failed migration puts on the update system. Explains the block first (which script, which error, whether a run is in progress), then clears it;--dry-runto look without touching,--skip-version=<v>to mark an unfixable script as applied without running it.configbox:run-task <controller> [task] [key=value…] [--user …] [--output-mode …]— run any CB controller task from the CLI exactly as the/cbx-api/endpoint would (inject GET/POST-style params, impersonate a user so CB behaves as if they're logged in, capture the chrome-free output).configbox:config:get|set|list— read/write admin settings throughConfigboxModelConfig, so asetruns the same validation a UI save runs; handles translatables per--language;listflags which settings are editable.configbox:sysvar:get|set|list— the low-level#__configbox_system_varsbookkeeping store.
Reusable core: all the logic is in a platform-agnostic ConfigboxCliHelper (Kenedo, no Joomla), so the
same commands can be re-exposed on WordPress (WP-CLI) and Magento with thin wrappers. Registration is a
console-group plugin (Joomla 5 has no component console-command tag).
Read more: the CLI command suite — every command with its arguments, options and exit codes, the core/wrapper split, and the WordPress and Magento invocations · what the suite has gained since · adding your own commands in the customization layer
Admin light / dark / auto theming
- The backend supports light/dark/auto, anchored on the standard
data-bs-themeattribute (Bootstrap 5.3 colour-mode). On Joomla it inherits the Atum template's choice automatically; other platforms resolve it via a newKenedoPlatform::getAdminColorScheme()method (prefers-color-schemefallback for auto). - Admin colours are now CSS custom properties (
--cb-*) inassets/css/admin-theme.css— a real token layer with light/dark/auto blocks and vendor (Tom Select / jQuery UI) overrides. Custom admin CSS should reference these tokens so it flips with the scheme.
Read more: admin theming — the --cb-* token
layer — how each platform resolves the scheme, the full
token set, the vendor chrome, and the rules custom admin CSS has to follow · host-adaptation
stylesheets · the dark-mode entry in the
breaking-changes log
Better migration ergonomics
-
The whole update-script workflow is written down: the idempotency helpers (
tableExists/tableFieldExists/keyExists/getFkConstraintName), thefailed_update_detectedfreeze, the dedicatedconfigbox_upgrade_errors.log, and the CLI debug loop. -
Migrations can now be driven explicitly from the CLI (
configbox:migrate) instead of only implicitly on init — so a deploy step surfaces failures in your deploy log, not to the first visitor. -
Pending migrations are now visible. CBX tracks its schema version in
#__configbox_system_vars, not in the host's extension registry — which is why Joomla's extension version and Magento'ssetup:db:statusboth report "up to date" while CBX migrations are outstanding, and the only symptom is a 500 on a product page.configbox:migrate --statusreports the installed version, the newest shipped one, and every pending core/customization script, applies nothing, and exits non-zero when work is due — so a deploy or CI step can gate on it:php cli/joomla.php configbox:migrate --statusThe equivalent raw check is
SELECT value FROM #__configbox_system_vars WHERE \key` = 'latest_update_version', compared against the newest file inhelpers/updates/`. -
Ordering to be aware of:
applyUpdates()runs all core scripts, then all customization scripts. Harmless on an existing site; on a fresh install it means core migrates to current (renaming#__configbox_elements) before your historical customization scripts run against the new schema. Write customization update scripts defensively, or collapse them into a current-vocabulary baseline.
Read more: database migrations — writing an idempotent update script, both tracks, the freeze and how to lift it, and the debug loop · the CLI reference · what deploy tooling has gained since
Customization compatibility as a first-class concern
- The core ships auto-conversion shims for the answer collapse (model aliases, legacy customization-file loading with key remaps, graceful log-and-skip instead of white-screening).
- This
migration-to-cb4/docs area is the standing home for every breaking change: a running log, a per-kind playbook, a deep dive, and per-audience orientation. New breaking changes get logged here as a rule (see the README's rules section).
Read more: the customization layer, in full — every extension point, where each resolves, and which mechanism to reach for · model-property customizations after the collapse · what the layer can now extend
Housekeeping you should know about
- Public pricing API renamed
ConfigboxPrices::getXref*()→getAnswer*(); cache keys*ByXref→*ByAnswerandxref_to_*→answer_to_question(the answer-collapse first producedanswer_to_element, then the element→question rename renamed it again; the old forms remain as aliases).
Read more: the old → new identifier cheat-sheet — every renamed identifier with its status (shim, manual, manual+data) and a grep pack to find the hits · the breaking-changes log
The "element" entity is now "question" (code + DB + engine)
- The configurator entity that the UI always called a question was still
elementin the code, DB and admin MVC. CBX 4 finishes the transition: table#__configbox_elements→#__configbox_questions, every*element_id*/feature column →question_*, modelConfigboxModelAdminelements→ConfigboxModelQuestions, pricing APIgetElement*()→getQuestion*(), assignment/pricing cache keyselement_*/*ByElement→question_*/*ByQuestion, the rule/calc classesConfigbox{Condition,CalcTerm}ElementAttribute→…QuestionProperty, and the calc DSL tokens (ElementAttribute(→QuestionProperty(, etc.). The entity id is unchanged. - The DB half ships as
helpers/updates/3.5.2.php(applied byconfigbox:migrate); the stored-rules transform is model-driven (it discovers and converts custom rule columns too). Backward-compat shims cover the model class, pricing methods, cache keys and the customization-file name; the rule/calc type strings and DSL tokens are a hard rename (stored data migrated, no alias).
Read more: the element → question rename, before → after — the full identifier map and the per-file decision procedure · the old → new cheat-sheet · the rule engine and the calculation engine, whose vocabularies changed with it