# Custom Rule Condition Types

> How to add your own condition type to the CBX rule engine — a new kind of "if…" that admins can drop into a rule in the Rule Editor and that the engine evalu…

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/customization/custom_rule_conditions. Last updated 2026-08-25.

---
How to add your own **condition type** to the CBX rule engine — a new kind of "if…" that admins can
drop into a rule in the Rule Editor and that the engine evaluates at configure-time to show/hide questions,
options, etc. CBX ships a handful of built-in condition types (selection-in-a-question, calculation
value, customer group, negation); this guide shows how to add a new one without touching core.

> **The engine is encoded; the condition *types* are not.** The rule engine itself
> (`ConfigboxRulesHelper`) ships **ionCube-encoded** (`helpers/encoded/`), so you cannot read or patch
> it — its behaviour is documented in
> [the rule engine reference](https://docs.configbox.at/docs/4.0-preview/technical/rule_engine) instead.
> But **condition types are plaintext PHP classes** that the engine
> calls into (`ConfigboxRulesHelper::getEvaluationResult()` → `ConfigboxCondition::getCondition($type)->getEvaluationResult()`).
> So you can add condition types freely — no encoder, no engine
> rebuild. This is the one place you extend rule behavior from the customization layer.

Read `com_configbox_customization_overview.md` first; `technical/com_configbox_rule_engine.md` for how the
engine stores and evaluates rules, and `functional/com_configbox_rule_authoring.md` for the admin's view of
authoring rules. 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. Start from the shipped template

CBX ships a fully-annotated reference condition type, **`CustomConditionExample`**, in the **core**
folder `classes/rule_condition_types/CustomConditionExample.php`. It documents the data shapes and every
method inline and is the intended starting point.

**Copy it into the customization folder and rename it** to your type:

```
classes/rule_condition_types/CustomConditionExample.php   (read this — the annotated reference)
        ↓ copy + rename
data/customization/rule_condition_types/CustomConditionWeekday.php
```

The rest of this guide explains the contract that template satisfies, so you understand what to change.

---

## 2. How a type resolves to your class

All condition classes are loaded eagerly at boot by `ConfigboxCondition::loadConditionClasses()`
(`classes/ConfigboxCondition.php:75-95`): every `.php` file in **core** `classes/rule_condition_types/`
*and* in **`data/customization/rule_condition_types/`** is `include_once`'d. Then, for a given type name,
`getCondition($type)` (`:34-69`) picks the class:

```php
$regularClass = 'ConfigboxCondition'.ucfirst($type);   // built-in naming
$customClass   = 'CustomCondition'.ucfirst($type);      // custom naming

if (class_exists($regularClass))      { $class = $regularClass; }   // built-in checked FIRST
elseif (class_exists($customClass))   { $class = $customClass; }     // then custom
else { throw … 'Custom condition class should be called "'.$customClass.'"'; }
```

The contract, therefore:

| Concern | Rule | Example (type `Weekday`) |
|---|---|---|
| **File** | any `.php` in `data/customization/rule_condition_types/` (the file *name* is free; the **class name** is what matters) | `CustomConditionWeekday.php` |
| **Class name** | **`CustomCondition` + `ucfirst(type)`** — note the `CustomCondition` prefix, *not* `ConfigboxCondition` | `class CustomConditionWeekday extends ConfigboxCondition` |
| **Type name** | derived back from the class name (`getTypeName()`, `:150`) — strip the `CustomCondition`/`ConfigboxCondition` prefix | `Weekday` |

> **Two naming rules that bite:**
> 1. **Use the `CustomCondition` prefix.** A custom class named `ConfigboxConditionWeekday` would also be
>    found (built-in branch), but you'd be impersonating the built-in namespace — and a built-in of the
>    same name would shadow you, since built-in is checked **first** (`:47-52`). Stick to `CustomCondition`.
> 2. **`ucfirst(type)`** — the type string in the rule data is matched case-folded on the first letter.
>    Keep your `data-type` attribute (§4) consistent with the class suffix (`Weekday` ↔
>    `CustomConditionWeekday`).
>
> The whole-folder eager include means **don't put non-class code at file top-level** — every file runs at
> boot. Guard with `defined('CB_VALID_ENTRY') or die();` and define only the class.

---

## 3. The contract — methods you implement

`ConfigboxCondition` (`classes/ConfigboxCondition.php`) is `abstract` with **three required** methods and
several optional hooks with working defaults.

### Required (abstract)

| Method | Returns | Purpose |
|---|---|---|
| **`getEvaluationResult($conditionData, $selections)`** (`:197`) | `bool` | **The core logic.** `true` when the condition is met for the current configuration. Called by the engine every time a selection changes — keep it fast. |
| **`getConditionsPanelHtml($ruleEditorView)`** (`:203`) | HTML string | The type's **panel** in the Rule Editor — the palette of conditions the admin can drag into a rule. |
| **`getConditionHtml($conditionData, $forEditing = true)`** (`:213`) | HTML string | Renders **one condition** — as an editable widget (`$forEditing = true`) or as read-only display elsewhere in the backend. |

### Optional (override to change defaults)

| Method | Default | Override when |
|---|---|---|
| `getOperators()` (`:177`) | `<, <=, ==, !=, >=, >` (with readable text) | your type needs different/relational operators (or a single fixed one). |
| `getTypeTitle()` (`:143`) | `KText` of `CONDITION_TYPE_<name>` | you want a custom tab/panel title. |
| `showPanel()` (`:218`) | `true` | your type shouldn't show its own palette panel. |
| `containsQuestionId()` / `containsAnswerId()` / `containsCalculationId()` (`:231`,`:245`,`:259`) | `false` | your condition references a question/answer/calculation — so the engine knows the rule blocks deleting that entity. **Implement these if your `conditionData` holds such ids.** |
| `getCopiedConditionData($conditionData, $copyIds)` (`:270`) | returns data unchanged | your condition stores ids that must be remapped when a product/rule is **copied** (see the copy mechanism in `technical/com_configbox_mvc_tasks.md`). |

---

## 4. The two data shapes (know these cold)

Both are plain associative arrays; `CustomConditionExample::readMe()` (`:6-85`) documents them.

**`$selections`** — the customer's current configuration. Keys are **question ids**, values are the chosen
value (an entered string, or the selected answer's id):

```php
$selections = array(
    3 => 'ABC',   // customer typed "ABC" in question id 3
    6 => '4',     // customer chose the answer with id 4 in question id 6
);
```

**`$conditionData`** — one condition's stored data. It is built **from the `data-*` attributes** you emit
in `getConditionHtml()`. `type` and `operator` are required; everything else is yours:

```php
$conditionData = array(
    'type'        => 'Weekday',   // = your type name (the data-type attribute)
    'name'        => 'On a weekend',   // human label shown in the editor
    'operator'    => '==',        // machine-readable relational operator
    'shouldValue' => '6',         // your custom payload (any keys you like)
    // …any other data-* you defined…
);
```

**The bridge between HTML and data:** every `data-foo-bar` attribute on the `span.item.condition` becomes
`$conditionData['fooBar']` (kebab → camelCase). Inputs inside the condition use
`data-data-key="shouldValue"` to write back into the condition data. The Rule Editor's built-in JS handles
the operator picker and decimal-symbol normalization automatically — you just emit the markup conventions.

---

## 5. Worked example — a `Weekday` condition

A condition that is met when *today* is the weekday the admin picked. It needs no question/answer ids, so
the `contains*` and copy hooks keep their defaults.

```php
// data/customization/rule_condition_types/CustomConditionWeekday.php
<?php
defined('CB_VALID_ENTRY') or die();

/**
 * Met when the current weekday matches (operator) the admin-chosen weekday (0=Sun … 6=Sat).
 * Type string 'Weekday' ⇒ class CustomConditionWeekday.
 */
class CustomConditionWeekday extends ConfigboxCondition {

    /** Tab/panel title in the Rule Editor. */
    function getTypeTitle() {
        return KText::_('Day of week');
    }

    /** The palette: the conditions an admin can add for this type. */
    function getConditionsPanelHtml($ruleEditorView) {
        $seed = array('type' => 'Weekday', 'name' => KText::_('Day of week'), 'operator' => '==', 'shouldValue' => '6');
        ob_start(); ?>
        <ul class="conditions-list">
            <li><?php echo $this->getConditionHtml($seed); ?></li>
        </ul>
        <?php
        return ob_get_clean();
    }

    /** Render one condition — editable widget or read-only display. */
    function getConditionHtml($conditionData, $forEditing = true) {

        $days = array('0'=>KText::_('Sunday'),'1'=>KText::_('Monday'),'2'=>KText::_('Tuesday'),
                      '3'=>KText::_('Wednesday'),'4'=>KText::_('Thursday'),'5'=>KText::_('Friday'),'6'=>KText::_('Saturday'));
        $current = isset($conditionData['shouldValue']) ? (string)$conditionData['shouldValue'] : '6';

        ob_start(); ?>
        <span class="item condition"
              data-type="<?php echo hsc($conditionData['type']); ?>"
              data-name="<?php echo hsc($conditionData['name']); ?>"
              data-operator="<?php echo hsc($conditionData['operator']); ?>">

            <span class="condition-name"><?php echo hsc($conditionData['name']); ?></span>
            <span class="condition-operator"><?php echo $this->getOperatorText($conditionData['operator']); ?></span>

            <?php if ($forEditing): ?>
                <select class="input" data-data-key="shouldValue">
                    <?php foreach ($days as $val => $label): ?>
                        <option value="<?php echo hsc($val); ?>" <?php echo ($val === $current) ? 'selected' : ''; ?>><?php echo hsc($label); ?></option>
                    <?php endforeach; ?>
                </select>
            <?php else: ?>
                <span class="condition-value"><?php echo hsc($days[$current]); ?></span>
            <?php endif; ?>
        </span>
        <?php
        return ob_get_clean();
    }

    /** The logic: compare today's weekday against the chosen one with the chosen operator. */
    function getEvaluationResult($conditionData, $selections) {

        $today    = (int)date('w');                       // 0..6
        $should   = (int)$conditionData['shouldValue'];
        $operator = $conditionData['operator'];

        switch ($operator) {
            case '<':  return $today <  $should;
            case '<=': return $today <= $should;
            case '!=': return $today != $should;
            case '>=': return $today >= $should;
            case '>':  return $today >  $should;
            case '==':
            default:   return $today == $should;
        }
    }
}
```

What we relied on the base class for: operator text/list (`getOperators()`/`getOperatorText()`), the type
name derivation, and the no-op `contains*`/copy hooks (this type holds no entity ids). Drop the file in
place, reload the Rule Editor, and the new type's panel appears.

> **If your condition *does* reference questions/answers/calculations** (e.g. you store a `questionId` in
> `conditionData`, like the shipped `CustomConditionLof`), you **must** implement the matching `contains*`
> method (return whether that id appears) so the engine knows the rule depends on it and prevents deleting
> it; and implement `getCopiedConditionData()` to remap the id when products/rules are copied. Skipping
> these leads to dangling references after deletes/copies.

---

## 6. Performance — `getEvaluationResult` runs a lot

`getEvaluationResult()` is called by the engine **every time the visitor changes a selection**, for **every
rule** that uses your condition (`rules.php:194`). The engine memoizes rule results, but your method body
runs per evaluation. So:

- Keep it **cheap and side-effect-free**; do simple comparisons on data already in `$conditionData` /
  `$selections`.
- If you must load external data (customer record via `ConfigboxUserHelper::getUser()`, a DB lookup),
  **cache it statically** within the request — the example's `readMe()` calls this out explicitly
  (`:186-188`).
- Never write to the DB or session here.

---

## 7. Deployment checklist

```
data/customization/
  rule_condition_types/
    CustomCondition<Type>.php     ← class CustomCondition<Type> extends ConfigboxCondition
```

1. **Copy the reference** `classes/rule_condition_types/CustomConditionExample.php` into
   `data/customization/rule_condition_types/` and rename file + class to your type.
2. **Name the class `CustomCondition<Ucfirst(type)>`** and keep `data-type="<Type>"` consistent (§2).
3. **Implement the three required methods** (`getEvaluationResult`, `getConditionsPanelHtml`,
   `getConditionHtml`); override operators/title only if needed (§3).
4. **Emit the markup conventions** in `getConditionHtml()`: `span.item.condition`, required `data-type` +
   `data-operator`, `.input` with `data-data-key` for editable values, `.condition-name`/`.condition-operator`
   (§4).
5. **Implement `contains*` + `getCopiedConditionData()`** if your condition stores question/answer/
   calculation ids (§5).
6. **Keep `getEvaluationResult()` fast** and cache any external lookups (§6).
7. **Verify manually** — add the condition to a rule in the Rule Editor, save, reconfigure on the frontend
   and confirm the show/hide fires; then delete/copy the product and confirm no dangling reference.
   CBX has **zero automated tests**.

---

## 8. Conventions & gotchas

- **`CustomCondition` prefix, not `ConfigboxCondition`.** Built-in classes are checked first; the custom
  branch needs the `CustomCondition` name (`:45-52`).
- **Every file in the folder runs at boot** (eager include). One class per file, no top-level side effects,
  always `defined('CB_VALID_ENTRY') or die();`.
- **`data-*` ⇒ `conditionData` (kebab→camelCase); `type` & `operator` are required.** Inputs write back via
  `data-data-key`. The editor JS handles the operator picker and decimal normalization.
- **Implement `contains*` if you hold ids** — otherwise the engine can't protect referenced entities from
  deletion, and copy won't remap ids.
- **`getEvaluationResult` is hot** — cheap, cached, no side effects (§6).
- **The engine is encoded but condition types aren't** — you don't need the ionCube encoder or the
  `unencoded/` sources to add a condition type.
- **Escape output with `hsc()`** in the HTML methods; this is admin-facing markup but still escape stored
  values.

---

## See also

- `classes/rule_condition_types/CustomConditionExample.php` — the annotated reference template (start here).
- `classes/rule_condition_types/ConfigboxCondition*.php` — built-in types (Calculation, CustomerGroup,
  ElementAttribute, Negation) to copy patterns from.
- `classes/ConfigboxCondition.php` — the base class: the resolution (`getCondition`, `:34`), the loader
  (`:75`), and every overridable hook with its default.
- `technical/com_configbox_rule_engine.md` — how rules are stored and evaluated (the engine that calls your
  condition).
- `functional/com_configbox_rule_authoring.md` — the admin's view of building rules with conditions.
- `com_configbox_customization_overview.md` — the extension-point map.
