Custom Rule Condition Types
- Version
- 3.x
- Updated
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 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:
$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:
- Use the
CustomConditionprefix. A custom class namedConfigboxConditionWeekdaywould 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 toCustomCondition.ucfirst(type)— the type string in the rule data is matched case-folded on the first letter. Keep yourdata-typeattribute (§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):
$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:
$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.
// 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
questionIdinconditionData, like the shippedCustomConditionLof), you must implement the matchingcontains*method (return whether that id appears) so the engine knows the rule depends on it and prevents deleting it; and implementgetCopiedConditionData()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'sreadMe()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
- Copy the reference
classes/rule_condition_types/CustomConditionExample.phpintodata/customization/rule_condition_types/and rename file + class to your type. - Name the class
CustomCondition<Ucfirst(type)>and keepdata-type="<Type>"consistent (§2). - Implement the three required methods (
getEvaluationResult,getConditionsPanelHtml,getConditionHtml); override operators/title only if needed (§3). - Emit the markup conventions in
getConditionHtml():span.item.condition, requireddata-type+data-operator,.inputwithdata-data-keyfor editable values,.condition-name/.condition-operator(§4). - Implement
contains*+getCopiedConditionData()if your condition stores question/answer/ calculation ids (§5). - Keep
getEvaluationResult()fast and cache any external lookups (§6). - 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
CustomConditionprefix, notConfigboxCondition. Built-in classes are checked first; the custom branch needs theCustomConditionname (: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&operatorare required. Inputs write back viadata-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. getEvaluationResultis 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.