Skip to main content
Version: 4.0 preview

Custom Rule Condition Types

Version
4.0 preview
Updated
View markdown

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:

ConcernRuleExample (type Weekday)
Fileany .php in data/customization/rule_condition_types/ (the file name is free; the class name is what matters)CustomConditionWeekday.php
Class nameCustomCondition + ucfirst(type) — note the CustomCondition prefix, not ConfigboxConditionclass CustomConditionWeekday extends ConfigboxCondition
Type namederived back from the class name (getTypeName(), :150) — strip the CustomCondition/ConfigboxCondition prefixWeekday

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 (WeekdayCustomConditionWeekday).

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)

MethodReturnsPurpose
getEvaluationResult($conditionData, $selections) (:197)boolThe 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 stringThe type's panel in the Rule Editor — the palette of conditions the admin can drag into a rule.
getConditionHtml($conditionData, $forEditing = true) (:213)HTML stringRenders one condition — as an editable widget ($forEditing = true) or as read-only display elsewhere in the backend.

Optional (override to change defaults)

MethodDefaultOverride 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)trueyour type shouldn't show its own palette panel.
containsQuestionId() / containsAnswerId() / containsCalculationId() (:231,:245,:259)falseyour 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 unchangedyour 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 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.