Skip to main content
Version: 3.x

Playbook: model-property customizations after the answer/option collapse

Version
3.x
Updated
View markdown

Applies to: files under data/customization/model_property_customization/ that customize the removed option/answer models. Prerequisite reading: the general customization guides ../customization/com_configbox_extending_stock_models.md (the base mechanism) and ../customization/com_configbox_custom_properties.md — both in the CBX developer docs (docs/customization/ in the platform integration repos, e.g. the Joomla site repo).


1. What the mechanism is (recap)

A file model_property_customization/<base>.php defining a function customPropertyDefinitions<Base>() gets merged into the model whose class is ConfigboxModel<Base>. Example: adminanswers.phpcustomPropertyDefinitionsAdminanswers()ConfigboxModelAnswers. The loader is KenedoModel::getCustomPropertyDefinitions().

2. What changed in CBX 4

Three models were merged into one — ConfigboxModelAnswers (table #__configbox_answers):

Removed modelOld file baseWas
ConfigboxModelAdminoptionsadminoptionsthe shared "global option"
ConfigboxModelAdminoptionassignmentsadminoptionassignmentsthe option↔question assignment editor
ConfigboxModelAdminxrefelementoptionsadminxrefelementoptionsthe per-question answer ("xref")

So a customization file bound to any of those three bases now targets a model that no longer exists — the adminanswers model would never load it.

3. What the core does for you automatically 🟢🟡

You do not have to move your code for the common cases. ConfigboxModelAnswers overrides two hooks so the old files keep working:

// models/adminanswers.php
function getLegacyCustomizationAliases() {
return array('adminxrefelementoptions', 'adminoptions', 'adminoptionassignments');
}

KenedoModel::getCustomPropertyDefinitions() loads the answers model's own file plus each alias file, merges their returned defs, and:

  • Repoints external storage: any def with storeExternally and foreignTableKey => 'option_id' is rewritten to foreignTableKey => 'answer_id' (via ConfigboxModelAnswers::normaliseLegacyCustomPropertyDefinitions()).
  • Drops the removed join: a def keyed option_id (the old "Reused Answer" join) is dropped with a log line — it has no equivalent on a 1:n answer.
  • Fails safe: if a legacy function throws (because it references a dropped table or a deleted property), it is logged to the CBX error log and skipped — the admin form still renders; you just lose that file's contributions until you fix them.

Additionally, KenedoModel::getModel() aliases the three old class names to ConfigboxModelAnswers, so a legacy file line like KenedoModel::getModel('ConfigboxModelAdminoptions')->getPropertyDefinitions() resolves to the answers model instead of fataling.

Net effect: a cleanly-written option/answer customization keeps working untouched (with a deprecation line in the log). Only files that reach into something genuinely removed need your hand.

4. What you still must fix by hand 🔴

Anything the shim logs-and-skips. The tell is a line in the CBX error log:

Skipping customization function "customPropertyDefinitionsAdminxrefelementoptions" for model
"ConfigboxModelAnswers" - it errored (likely references something removed by a core refactor): ...

Fix them by consolidating into a single, correct adminanswers.php. The transforms:

Move your definitions into one model_property_customization/adminanswers.php with function customPropertyDefinitionsAdminanswers(), and delete the three old files. This is the intended destination: it drops the deprecation log lines, leaves one authoritative file instead of three, and is what eventually lets core retire the alias shims.

The shim means the upgrade is not gated on doing this — getLegacyCustomizationAliases() keeps the old filenames working deliberately, so deferring is legitimate if you have bigger things in flight. But deferring is the only alternative; there is no reason to keep the old names permanently. When you do it, two things fail silently if you do them half-way:

  1. The function name must move with the filename. The loader derives the function name from the file's base name and silently skips the file when it is missing — no error, no log. Renaming adminoptions.phpadminanswers.php while leaving customPropertyDefinitionsAdminoptions() in place means your properties simply stop being registered and the admin form quietly loses fields. Rename both or neither.
  2. You inherit the compatibility rewriting. Definitions loaded under a legacy base are passed through normaliseLegacyCustomPropertyDefinitions(), which repoints external storage from 'foreignTableKey' => 'option_id' onto 'answer_id' for you. That runs only for legacy bases — the model's own file is assumed correct. Once the file is called adminanswers.php, do that repointing by hand (§4.4).

So: rename file and function together, hand-correct the foreignTableKeys in the same pass, then load the admin form and confirm your custom fields still render. The one state to avoid is a partial rename — file moved, function not — which silently deregisters your properties and is worse than either having done it or having left it alone.

4.2 Fix references to removed models / properties

// BEFORE (adminoptions.php) — makes the option SKU required
$regularProps = KenedoModel::getModel('ConfigboxModelAdminoptions')->getPropertyDefinitions();
$propDefs['sku'] = $regularProps['sku'];
$propDefs['sku']['required'] = '1';

// AFTER (adminanswers.php) — sku now lives on the answer
$regularProps = KenedoModel::getModel('ConfigboxModelAnswers')->getPropertyDefinitions();
$propDefs['sku'] = $regularProps['sku'];
$propDefs['sku']['required'] = '1';

The option_id "Reused Answer" join is gone — there is no shared option, so any block that re-exports, re-types (e.g. to bcoptionjoin), or relabels option_id must be deleted:

// BEFORE (adminxrefelementoptions.php) — DELETE THIS ENTIRE BLOCK
$originalDefs = KenedoModel::getModel(ConfigboxModelAdminxrefelementoptions::class)->getPropertyDefinitions();
$optionIdDef = $originalDefs['option_id'] ?? null;
if (!$optionIdDef) { throw new Exception('Could not find option_id property definition for overriding'); }
$optionIdDef['type'] = 'bcoptionjoin';
$propDefs['option_id'] = $optionIdDef;

4.3 Fix raw SQL against dropped tables

// BEFORE
$query = "SELECT `element_id` FROM `#__configbox_xref_element_option` WHERE `id` = ".intval($answerId);
// AFTER — note BOTH change: the table (collapse) and the column (element→question rename)
$query = "SELECT `question_id` FROM `#__configbox_answers` WHERE `id` = ".intval($answerId);

#__configbox_options is gone entirely — its columns (sku, price, weight, option_image, option_custom_1..4, availability, was-prices, …) are now columns on #__configbox_answers. Rewrite any optionsxref join to a plain read from #__configbox_answers.

4.4 Fix externally-stored fields (the data half)

The code half is auto-remapped (option_idanswer_id), but the external table itself is not. If a def stores into e.g. #__configbox_external_option_appends keyed by option_id, that table needs:

  1. an answer_id column, and
  2. its rows re-keyed from the old option id to the answer id.

Because one former option could be shared by several answers, this is a fan-out: each answer that used option O gets its own append row copied from O's row. (This mirrors what the core answer migration did for the title/description strings.) Deliver it as a data/customization/updates/ migration — see ../customization/com_configbox_extending_stock_models.md §"the migration that delivers the column" (in the CBX developer docs). If you do not migrate the data, the fields render but read/write against a missing/empty answer_id column.

4.5 Fix string keys (translatable custom fields)

Answer title/description/custom strings (#__configbox_strings types 5/15/60/61) are now keyed by the answer id, not the option id. A customization that reads them directly must key by the answer id:

// BEFORE
ConfigboxCacheHelper::getTranslation('#__configbox_strings', 5, $answer->option_id);
// AFTER
ConfigboxCacheHelper::getTranslation('#__configbox_strings', 5, $answer->id);

The positionForm bands the core answer model uses for the absorbed option fields are 46000–47800; place your inserted fields relative to those if you want them to sit inside the "Price, SKU and Weight" / availability / description groups.

4.6 Stop using a property's absence as a platform test

'platforms' used to remove a property from getProperties() on every other host. It no longer does — every property exists on every platform now (one schema, one set of migrations), and the key only hides the field and drops its required where it does not apply. So any check shaped like "is this property here?" answers yes everywhere from CBX 4:

// BEFORE — worked because the property really was absent off Joomla
$props = $model->getProperties();
if (isset($props['joomla_user_group_id'])) {} // now ALWAYS true

// AFTER — ask the question you actually mean
if (KenedoPlatform::getName() === 'joomla') {} // "am I on Joomla?"
if ($props['joomla_user_group_id']->appliesToThisPlatform()) {} // "does this field apply here?"

Reading or writing such a field off-platform is safe rather than fatal — the column exists on every host, the value round-trips, and nothing consumes it. What changed is only that absence is no longer the signal.

If your own definition declares 'platforms', it keeps working and keeps its intent: not in the form, never required. Add 'platformDefaults' => array('magento2' => 0) beside it when a host also needs a new record to start at a different value — the per-platform if blocks that used to set ['invisible'] and ['default'] together are both declarations now. Note that getPropertyDefinition('default') resolves it, so that accessor answers for the running platform; read $definition['default'] when you want the neutral declaration.

Full treatment: ../technical/com_configbox_property_definition_settings.md §11–12.

4b. If your customization introduces a property TYPE, it needs a template 🔴

A property definition that names a type core does not ship — 'type' => 'colorpicker' — is a custom property type, and a custom property type without a template is a broken admin screen.

KenedoProperty::getBodyAdmin() resolves data/customization/properties/tmpl/<lowercase-type>.php, falling back to core's external/kenedo/properties/tmpl/. If neither exists it throws, and because the form renders its properties in one pass, one such property takes the whole edit screen down — not just its own field. The symptom is a dead admin form, which reads like a much bigger problem than a missing file.

The exception message names the three ways to satisfy it, and one of them must be true before you ship:

  1. ship data/customization/properties/tmpl/<lowercase-type>.php;
  2. override getAdminTemplateFile() to reuse another type's template, when yours is a thin variant;
  3. override getBodyAdmin() and return the HTML yourself — for display-only properties or widgets built entirely in PHP/JS.

Watch for it especially when a property definition is copied between models during migration: the definition travels, the template does not, and the type only fails on the screen that renders it — which may not be the screen you were testing.

Older documentation described the template as optional, with a missing one rendering an empty body. That was wrong; it throws. See ../customization/com_configbox_custom_properties.md §2.3 for the full treatment.


5. Verification checklist

  • Load an answer edit form (controller=adminanswers&task=edit&id=<answerId>) — it renders, no white screen.
  • The CBX error log has no Skipping customization function … line for your files.
  • Your custom fields appear on the form and their values save (check the external table gets an answer_id-keyed row).
  • Every custom property type you introduce has a template, a getAdminTemplateFile() override or a getBodyAdmin() override (§4b) — a missing one kills the whole form, not just the field.
  • No code of yours treats a property's absence from getProperties() as a platform test (§4.6) — every property exists on every host now, so such a check is always true.
  • Grep your customization for the removed identifiers and confirm none remain in code you own: xref_element_option, configbox_options, option_id, ConfigboxModelAdminoptions, ConfigboxModelAdminoptionassignments, ConfigboxModelAdminxrefelementoptions, getXref.

6. Reference: the exact old → new mapping

OldNew
ConfigboxModelAdminoptions / ...Adminoptionassignments / ...AdminxrefelementoptionsConfigboxModelAnswers
file adminoptions.php / adminoptionassignments.php / adminxrefelementoptions.phpadminanswers.php
function customPropertyDefinitionsAdmin{options,optionassignments,xrefelementoptions}()customPropertyDefinitionsAdminanswers()
#__configbox_options, #__configbox_xref_element_option#__configbox_answers
foreignTableKey => 'option_id'foreignTableKey => 'answer_id'
option_id join property ("Reused Answer")removed
strings keyed by option_id (types 5/15/60/61)keyed by the answer id