# Custom Calc Term Types

> How to add your own calculation term type to CBX — a new kind of building block an admin can drop into a formula calculation, which the pricing engine evalua…

Source: CBX documentation, version 3.x (released). Canonical page: https://docs.configbox.at/docs/customization/custom_calc_term_types. Last updated 2026-08-03.

---
How to add your own **calculation term type** to CBX — a new kind of building block an admin can
drop into a formula calculation, which the pricing engine evaluates at configure-time. CBX ships
six term types (number, operator, function, calculation, question property, customer group); this
guide shows how to add one without touching core.

This is the natural companion to `com_configbox_custom_question_types.md`: a custom question type
that stores a composite value (a width/height/quantity triple, say) cannot be priced by the shipped
terms, because they reduce a selection with `floatval()`. A custom term is how that value reaches a
price.

Read `com_configbox_customization_overview.md` first, and
`technical/com_configbox_calculation_engine.md` for how calculations are stored and evaluated. All
paths are relative to the component root `docroot/components/com_configbox/`;
`getDirCustomization()` is `KenedoPlatform::p()->getDirCustomization()`. Source references are
point-in-time — verify against the code.

---

## 1. How a term type resolves to your class

Unlike question types, term classes are loaded **eagerly**.
`ConfigboxCalcTerm::loadTermClasses()` (`classes/ConfigboxCalcTerm.php:79`) `include_once`s every
`.php` file in core `classes/calc_term_types/` **and** in
`getDirCustomization()/calc_term_types/`. There is no autoloader involvement and no registration
step — dropping the file in is the installation.

The type *name* is derived from the class name by stripping either accepted prefix
(`getTermTypeNames()`, `classes/ConfigboxCalcTerm.php:131`):

```php
$className = str_replace('ConfigboxCalcTerm', '', $className);   // built-ins
$className = str_replace('CustomCalcTerm',    '', $className);   // yours
```

So both prefixes work, and the convention is:

```
classes/calc_term_types/ConfigboxCalcTermNumber.php          ← shipped   → type "Number"
getDirCustomization()/calc_term_types/CustomCalcTermDimensions.php  ← yours → type "Dimensions"
```

That suffix is what gets stored as `"type":"Dimensions"` in the formula's JSON, so **renaming the
class breaks every calculation already using it**. Treat the suffix as a published identifier.

---

## 2. The contract — three abstract methods

`ConfigboxCalcTerm` is abstract with exactly three methods to implement
(`classes/ConfigboxCalcTerm.php:169-186`). Two of them are admin-side; only one runs on the
storefront.

| Method | Runs | Job |
|---|---|---|
| `getTermsPanelHtml($calculationId, $productId)` | Formula editor | The panel an admin uses to insert your term — pickers, inputs, whatever the term needs |
| `getTermHtml($termData, $forEditing = true)` | Formula editor | Render one existing term. `$forEditing` false means a read-only display |
| `getTermResult($termData, $selections, $regardingQuestionId = null, $regardingAnswerId = null, $allowNonNumeric = false)` | **Every price calculation** | The value the term contributes |

`$termData` is your own shape — whatever `getTermsPanelHtml()` wrote into the formula JSON, handed
back to you. Keep it small and stable; it is persisted.

---

## 3. `getTermResult()` runs a lot

A formula is evaluated on every selection change, for every position in the cart, and again on
cart and checkout pages. `getTermResult()` therefore sits in the hottest path CBX has.

- **No queries per call** where a cached read will do. Question data is already cached; reach for
  `ConfigboxQuestion::getQuestion()` rather than SQL.
- **Return a number.** Unless `$allowNonNumeric` is true, the engine is composing an arithmetic
  expression; returning a string or `null` propagates into the whole formula.
- `$selections` is the current configuration. Read from it rather than re-fetching state.

---

## 4. The editor methods have a failure mode worth knowing

`getTermHtml()` renders terms an admin saved earlier — including terms that point at a **question
which has since been deleted**. If your implementation resolves a question id without checking,
the lookup throws, and the whole formula editor fails to render: the admin cannot open the
calculation, and cannot delete the stale term either, because they cannot reach it.

Guard it:

```php
if (!ConfigboxQuestion::questionExists($questionId)) {
    return '<span class="cb-term cb-term-broken">' . KText::_('Deleted question') . '</span>';
}
```

`ConfigboxQuestion::questionExists()` (`classes/ConfigboxQuestion.php:155`) exists for exactly
this. A placeholder keeps the editor usable and lets the admin remove the term.

---

## 5. Worked shape — pricing a composite question

For a `dimensions` question storing `{"w":1200,"h":800,"qty":2}`, a term that exposes one
sub-value:

```php
defined('CB_VALID_ENTRY') or die();

class CustomCalcTermDimensions extends ConfigboxCalcTerm {

    /** The sub-values this term can pull out of a dimensions selection. */
    protected function getSubValues() {
        return ['w' => KText::_('Width'), 'h' => KText::_('Height'), 'qty' => KText::_('Quantity')];
    }

    public function getTermResult($termData, $selections, $regardingQuestionId = null, $regardingAnswerId = null, $allowNonNumeric = false) {

        if (empty($termData['questionId']) || empty($selections[$termData['questionId']])) {
            return 0;
        }

        $decoded = json_decode($selections[$termData['questionId']], true);
        $key     = $termData['subValue'] ?? 'w';

        return isset($decoded[$key]) ? (float) $decoded[$key] : 0;
    }

    // getTermsPanelHtml() — a question picker plus a sub-value dropdown
    // getTermHtml()       — "Width of «Frame size»", guarded per §4
}
```

The point is the decode: the selection is JSON, so the shipped question-property term would read
`0`. That decode is the entire reason the type exists.

---

## 6. Deployment checklist

1. `getDirCustomization()/calc_term_types/CustomCalcTerm<Type>.php` — implement all three methods.
2. Clear the CBX cache; on some platforms CBX keeps its own file cache the host's cache-clear does
   not touch.
3. Open a formula calculation in the admin — your panel should be offered alongside the shipped
   terms. If it is absent, the file is not in the folder the platform resolves (§1).
4. Insert the term, save, and check a price on the storefront — `getTermsPanelHtml()` working does
   not prove `getTermResult()` does.

---

## 7. Gotchas

- **The class-name suffix is a stored identifier.** Renaming the class orphans every formula using
  it (§1).
- **Both prefixes are stripped**, so `ConfigboxCalcTermFoo` and `CustomCalcTermFoo` both yield type
  `Foo` — do not ship both.
- **Guard question lookups in `getTermHtml()`** or a deleted question takes the formula editor
  down with it (§4).
- **Loading is eager and unconditional**: a parse error in your file breaks calculation loading
  everywhere, not just where the term is used.
- **`getTermResult()` is hot** — see §3.

## See also

- `com_configbox_custom_question_types.md` — the composite values these terms usually exist to read
- `com_configbox_customization_overview.md` — the layer, and where it lives per platform
- `technical/com_configbox_calculation_engine.md` — storage, the three calculation types, evaluation
- `com_configbox_custom_rule_conditions.md` — the same pattern for the rule engine
