# The element → question rename — before → after reference

> The exhaustive reference for the CBX 4 rename of the "element" entity to "question". Read this for any customization that touches the configurator question e…

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/migration-to-cb4/element-question-rename. Last updated 2026-08-01.

---
**The exhaustive reference for the CBX 4 rename of the "element" entity to "question".** Read this for
any customization that touches the configurator question entity — its model, DB table, columns, the
pricing/assignment caches, the rules/calc engine, or the calc-formula DSL. It carries the full before → after
tables plus the per-file decision procedure and the shim inventory you can verify against the core.

> This is the sibling of [answers-collapse-deep-dive.md](https://docs.configbox.at/docs/4.0-preview/migration-to-cb4/answers-collapse-deep-dive). That one is about
> the option/answer collapse; this one is about the element→question rename. A heavily-customized site is
> likely to have touched both.

---

## Why this change

The core entity was called **`element`** in the code, DB and admin MVC, but **`question`** everywhere in the
UI and the frontend DOM/JS. That split was legacy debt: the same thing had two names depending on which layer
you were in. CBX 4 finishes the transition — **`element` → `question` everywhere**: the DB table and
every `*element_id*`/feature column, the admin model/controller/view classes, the pricing/assignment cache
keys, the ionCube rules/calc engine, the rule/calc condition classes and their stored JSON, and the
calc-formula DSL tokens.

**The mental-model swap:** wherever you thought "element" (the configurator question a customer answers), it
is now "question". The entity's **id is unchanged** — a question keeps the same id its element had — so carts,
orders, rules, calc formulas and translations all keep pointing at the right rows. Only the *names* changed.

**Not touched** (do not "fix" these — they are not ours):
- Joomla core `#__extensions.element` and `#__updates.element` — a Joomla concept, unrelated.
- WordPress WPML translation ids read as `$translation->element_id` in `helpers/wordpress.php` — a WordPress
  concept, unrelated.

---

## The DB migration ships with the core

The schema + stored-data half is applied automatically by the versioned update script
**`helpers/updates/3.5.2.php`** (runs on init and via `php cli/joomla.php configbox:migrate`). It renames the
table, all columns and the 8 foreign keys, and rewrites the stored rule/calc JSON and the calc-code DSL text.
**It does not touch `data/customization/`** — your custom SQL, custom columns and custom code are yours to
realign using the tables below. If your customization added its **own** rules-bearing column, the migration's
model-driven discovery converts its stored JSON automatically (see the "stored data" section).

---

## Before → after — DB tables & columns

The core migration renames these for you. Update any **raw SQL** in your customization that names them.

### Table

| Before | After |
|---|---|
| `#__configbox_elements` | `#__configbox_questions` |

### The question-id foreign keys (every `*element_id*` column)

| Table | Before column | After column | FK |
|---|---|---|---|
| `#__configbox_answers` | `element_id` | `question_id` | (index only) |
| `#__cbcheckout_order_configurations` | `element_id` | `question_id` | (index only) |
| `#__configbox_cart_position_configurations` | `element_id` | `question_id` | FK, CASCADE/CASCADE |
| `#__configbox_calculation_codes` | `element_id_a` … `element_id_d` | `question_id_a` … `question_id_d` | 4 FKs |
| `#__configbox_calculation_matrices` | `column_element_id` | `column_question_id` | FK |
| `#__configbox_calculation_matrices` | `row_element_id` | `row_question_id` | FK |
| `#__configbox_calculation_matrices` | `multielementid` | `multiquestionid` | FK |

### Feature columns (renamed on every table that carried them, incl. frozen order history)

| Table | Before | After |
|---|---|---|
| `#__configbox_questions` | `element_custom_1` … `element_custom_4` | `question_custom_1` … `question_custom_4` |
| `#__configbox_questions` | `element_css_classes` | `question_css_classes` |
| `#__cbcheckout_order_configurations` | `element_custom_1` … `element_custom_4` | `question_custom_1` … `question_custom_4` |
| `#__cbcheckout_order_configurations` | `element_type` | `question_type` |
| `#__cbcheckout_order_configurations` | `element_code` | `question_code` |
| `#__configbox_config` | `label_element_custom_1` … `_4` | `label_question_custom_1` … `_4` |
| `#__configbox_config` | `label_element_custom_translatable_1` / `_2` | `label_question_custom_translatable_1` / `_2` |
| `#__configbox_config` | `pm_regular_show_elements`, `pm_recurring_show_elements` | `pm_regular_show_questions`, `pm_recurring_show_questions` |
| `#__configbox_config` | `pm_regular_show_elementprices`, `pm_recurring_show_elementprices` | `pm_regular_show_questionprices`, `pm_recurring_show_questionprices` |
| `#__configbox_products` | `pm_regular_show_elements` / `_elementprices` (+ recurring) | `pm_regular_show_questions` / `_questionprices` (+ recurring) |

> **The frozen order-history columns are renamed too.** `cbcheckout_order_configurations` is a denormalized
> snapshot of placed orders — but only the **column names** changed; every value is preserved, and the row's
> `question_id` still equals the id it had as `element_id`. Custom code that reads order configs must use the
> new column names.

---

## Before → after — admin model / controller / views

The **model class was renamed** (`ConfigboxModelAdminelements` → `ConfigboxModelQuestions`); file and
view dirs moved with it.

| Kind | Before | After | Status |
|---|---|---|---|
| Model class | `ConfigboxModelAdminelements` | `ConfigboxModelQuestions` | 🟢 `getModel()` aliases old→new |
| Model file | `models/adminelements.php` | `models/adminquestions.php` | — |
| Controller | `controllers/adminelements.php` (`ConfigboxControllerAdminelements`) | `controllers/adminquestions.php` (`…Adminquestions`) | 🔴 update `controller=adminelements` URLs |
| List view | `views/adminelements/` (`ConfigboxViewAdminelements`) | `views/adminquestions/` (`…Adminquestions`) | 🔴 update `view=`/`getView()` refs |
| Form view | `views/adminelement/` (`ConfigboxViewAdminelement`) | `views/adminquestion/` (`…Adminquestion`) | 🔴 update `view=`/`getView()` refs |
| Copy-remap bucket | `$copyIds['adminelements']` | `$copyIds['adminquestions']` | 🔴 rename in custom copy code |
| Answer→question join prop | `element_id` (join to `adminelements`) | `question_id` (join to `adminquestions`) | 🔴 update filter names |
| Derived join-prop names | `joinedby_element_id_to_adminelements_*` | `joinedby_question_id_to_adminquestions_*` | 🔴 rename if you read these props |
| `model_property_customization/` file | `adminelements.php` | still loads (see below) | 🟢 auto-loaded via legacy alias |

**Editor sub-views** for the rule/calc "question attribute" term also moved:

| Before | After |
|---|---|
| `views/adminruleeditor_elementattribute/` | `views/adminruleeditor_questionproperty/` |
| `views/admincalcformula_elementattribute/` | `views/admincalcformula_questionproperty/` |

---

## Before → after — pricing API (public methods on `ConfigboxPrices`)

| Before | After | Status |
|---|---|---|
| `ConfigboxPrices::getElementPrice($id, …)` | `getQuestionPrice($id, …)` | 🟢 legacy alias kept |
| `ConfigboxPrices::getElementPriceRecurring($id, …)` | `getQuestionPriceRecurring($id, …)` | 🟢 legacy alias kept |
| `ConfigboxPrices::getElementWeight($id)` | `getQuestionWeight($id)` | 🟢 legacy alias kept |

The old names still work (thin aliases, each marked `@deprecated`); rename your calls at leisure.

---

## Before → after — the in-memory caches (assignment map + pricing cache)

These are the string keys of the per-request caches (`ConfigboxCacheHelper::getAssignments()` and the pricing
cache). They are rebuilt from the DB each request — **no stored state**, so this is a code-only change. The
core keeps the old keys populated as aliases, so cleanly-written cache readers keep working.

| Cache | Before key | After key | Status |
|---|---|---|---|
| assignments | `element_to_product` | `question_to_product` | 🟢 old key kept as alias |
| assignments | `product_to_element` | `product_to_question` | 🟢 old key kept as alias |
| assignments | `element_to_page` | `question_to_page` | 🟢 old key kept as alias |
| assignments | `page_to_element` | `page_to_question` | 🟢 old key kept as alias |
| assignments | `answer_to_element` | `answer_to_question` | 🟢 old key kept as alias |
| assignments | `element_to_answer` | `question_to_answer` | 🟢 old key kept as alias |
| pricing | `calcModelByElement` | `calcModelByQuestion` | 🟢 old key kept as alias |
| pricing | `calcModelRecurringByElement` | `calcModelRecurringByQuestion` | 🟢 old key kept as alias |
| pricing | `calcModelWeightByElement` | `calcModelWeightByQuestion` | 🟢 old key kept as alias |
| pricing | `taxClassIdByElement` | `taxClassIdByQuestion` | 🟢 old key kept as alias |
| pricing | `taxClassIdByElementRecurring` | `taxClassIdByQuestionRecurring` | 🟢 old key kept as alias |
| pricing | `calcModel*ByAnswer[…]['regardingElement']` | `['regardingQuestion']` | 🟢 old sub-key kept as alias |

> The old keys are **deprecated** — update your cache readers to the `question_*` keys; the aliases will be
> removed in a future release.

---

## Before → after — the rules / calc engine (ionCube) + stored JSON

The rule condition and calc term for "a question's attribute/selection" were renamed. This is a **hard rename
with no runtime alias** — instead the core migration rewrites the stored JSON so the new classes receive the
migrated data.

| Kind | Before | After | Status |
|---|---|---|---|
| Rule condition class | `ConfigboxConditionElementAttribute` | `ConfigboxConditionQuestionProperty` | 🔴 rename a custom subclass/ref |
| Calc term class | `ConfigboxCalcTermElementAttribute` | `ConfigboxCalcTermQuestionProperty` | 🔴 rename a custom subclass/ref |
| Rule/calc JSON `type` value | `"ElementAttribute"` | `"QuestionProperty"` | 🟢 migrated in stored data |
| Rule/calc JSON key | `"elementId"` | `"questionId"` | 🟢 migrated in stored data |
| Method on those classes | `getElementAttributes()` | `getQuestionProperties()` | 🔴 rename if you call it |

The dispatcher builds the class name from the stored `type` string (`ConfigboxCondition` + `ucfirst($type)`),
so a stored `"type":"QuestionProperty"` resolves to `ConfigboxConditionQuestionProperty`. That is why the
class rename **requires** the stored-JSON migration — they must move together. The sentinel value
`"questionId":"regarding"` (meaning "the question this formula is attached to") is preserved verbatim.

### The calc-formula DSL tokens (user-authored calc code)

Hard rename, **no alias** — the migration rewrites the stored `calculation_codes.code` text.

| Before token | After token |
|---|---|
| `ElementAttribute(` | `QuestionProperty(` |
| `ElementEntry(` | `QuestionSelection(` |
| `ElementPrice(` | `QuestionPrice(` |
| `ElementPriceRecurring(` | `QuestionPriceRecurring(` |
| `RegardingElement(` | `RegardingQuestion(` |

DSL **field paths** that named a feature column also change: `QuestionProperty(22.element_custom_1)` →
`QuestionProperty(22.question_custom_1)` (the path resolves against the question's renamed property).

The **field-path prefix crumbs** changed too (part of the option→answer rename — see
[answers-collapse-deep-dive.md](https://docs.configbox.at/docs/4.0-preview/migration-to-cb4/answers-collapse-deep-dive)):

| Before prefix | After prefix |
|---|---|
| `selectedOption.` | `selectedAnswer.` |
| `regardingOption.` | `regardingAnswer.` |

The engine's `ConfigboxQuestion::getField()` now resolves the crumbs `selectedAnswer` / `regardingAnswer`
only — there is **no** alias for the old `*Option.` prefixes. The suffix after the dot (`.id`,
`.assignment_custom_N`, `.option_custom_N`, `.weight`, `.basePriceStatic`, …) is unchanged. If your
customization stores calc code outside the `calculation_codes` table (e.g. generated in custom code),
rewrite these prefixes and the `Element*(` tokens the same way.

---

## Stored data — what the core migrates, and what you may still owe

The migration (`3.5.2.php`) transforms **all stock stored rule/calc data**, and — importantly — is
**model-driven** for rules: it discovers every model that declares a `rule` (or `calculationOverride`)
property, reads its effective property definitions (merging your `model_property_customization/` overrides)
and transforms whichever table+column that property is stored in. **So if your customization added its own
`rule`-typed property on a model, its stored JSON is converted for you.** The raw `calculation_formulas.calc`
column and the `calculation_codes.code` DSL are transformed explicitly.

**What you may still owe** — a DATA migration the core cannot do for you:
- calc code / rule JSON your customization stores in a **table or column the core does not know about**
  (e.g. an external append table, a JSON blob in a custom settings row). The core migrates only columns it
  can discover via the model layer or the two explicit stock columns. Ship your own
  `data/customization/updates/` migration applying the same transforms (JSON key `elementId`→`questionId`,
  type value `ElementAttribute`→`QuestionProperty`; the field-path prefixes `selectedOption.`→`selectedAnswer.`
  and `regardingOption.`→`regardingAnswer.`; DSL tokens per the table above). See
  `../technical/com_configbox_migrations.md` in the CBX developer docs.

---

## Per-file decision procedure (for a heavily-customized site)

For each file under `data/customization/`:

1. **Is it loaded on this install?** Custom models/controllers/views only matter if this deployment uses
   them. `model_property_customization/adminelements.php` **is** still loaded — see the shim below.
2. **Which bucket?**
   - References only **renamed-but-shimmed** things (the `ConfigboxModelAdminelements` class name, the
     `element_*` cache keys, `getElementPrice*`, the `adminelements` customization filename) → 🟢 works today,
     but update at leisure; a deprecation is logged.
   - References **renamed things with no shim** — raw SQL on `#__configbox_elements` / the `*element_id*`
     columns / feature columns, `controller=adminelements`/`view=adminelement(s)` URLs, the
     `ConfigboxConditionElementAttribute`/`ConfigboxCalcTermElementAttribute` classes, the DSL tokens in
     calc code you generate → 🔴 must edit (see the tables above for each old → new).
3. **Does it also need a data migration?** If it stores rule/calc JSON in a column the core migration doesn't
   reach (step above), write a `data/customization/updates/` script. A code rename alone won't move that data.
4. **Prove it's clean** with this grep (should return nothing but intentional legacy-alias lines):
   ```
   grep -rnE "configbox_elements|element_id|element_custom_|element_css_classes|ConfigboxModelAdminelements|ConfigboxConditionElementAttribute|ConfigboxCalcTermElementAttribute|joinedby_element_id_to_adminelements|column_element_id|row_element_id|multielementid|ElementAttribute\(|ElementPrice\(|ElementEntry\(|RegardingElement\(|element_to_|_to_element|ByElement|regardingElement|getElementPrice|getElementWeight" data/customization/
   ```
   Exclude only your own intentional back-compat lines. Do **not** flag `helpers/wordpress.php`-style
   `$translation->element_id` (WPML) — that is a WordPress concept, not this entity.

---

## Worked conversions

**Raw SQL on the questions table:**
```php
// before
$db->setQuery("SELECT `id`, `element_custom_1` FROM `#__configbox_elements` WHERE `page_id` = ".$pid);
// after
$db->setQuery("SELECT `id`, `question_custom_1` FROM `#__configbox_questions` WHERE `page_id` = ".$pid);
```

**Answers joined to their question:**
```php
// before
"LEFT JOIN `#__configbox_elements` AS e ON e.id = answer.element_id"
// after
"LEFT JOIN `#__configbox_questions` AS q ON q.id = answer.question_id"
```

**Reading the assignment cache:**
```php
// before — still works via the alias, but deprecated
$questionIds = $assignments['product_to_element'][$productId];
// after
$questionIds = $assignments['product_to_question'][$productId];
```

**A calc-code DSL formula:**
```
// before
ElementAttribute(22.element_custom_1) * RegardingElement(regardingOption.basePriceStatic)
// after
QuestionProperty(22.question_custom_1) * RegardingQuestion(regardingAnswer.basePriceStatic)
```

**An admin edit-link URL in a custom view:**
```php
// before
'index.php?option=com_configbox&controller=adminelements&task=edit&id='.$id
// after
'index.php?option=com_configbox&controller=adminquestions&task=edit&id='.$id
```

---

## The core shims that do the auto-conversion (verify these)

- **`KenedoModel::getModel()`** — aliases `ConfigboxModelAdminelements` → `ConfigboxModelQuestions`
  (same pattern as the answers-collapse aliases). Logs a legacy call. So `getModel('ConfigboxModelAdminelements')`
  keeps returning the right model.
- **`ConfigboxModelQuestions::getLegacyCustomizationAliases()`** returns `array('adminelements')` — so the
  merge in `KenedoModel::getCustomPropertyDefinitions()` still loads your existing
  `model_property_customization/adminelements.php` and applies it to the renamed model. No need to rename that
  file.
- **`ConfigboxCacheHelper` assignment build** — re-exposes the old `element_*` assignment keys as aliases of
  the new `question_*` keys (`$legacyAssignmentKeyAliases`), and the old `*ByElement`/`regardingElement`
  pricing keys as aliases (`$legacyPricingKeyAliases`). Cleanly-written cache readers keep working.
- **`ConfigboxPrices::getElementPrice()` / `getElementPriceRecurring()` / `getElementWeight()`** — thin
  `@deprecated` aliases that call the `getQuestion*` methods.

**No shim** (hard rename, stored data migrated instead): the rule/calc **type strings** and the calc **DSL
tokens**. There is no runtime alias for `"ElementAttribute"` or `ElementAttribute(` — the migration rewrites
the stored data so the new classes/parsers receive it. If you author rule/calc JSON or DSL code in
customization at runtime, emit the new names.
