Skip to main content
Version: 4.0 preview

Looking up assignments: ConfigboxAssignmentsHelper

Version
4.0 preview
Updated
View markdown

Applies to: any customization that reads ConfigboxCacheHelper::getAssignments(). Status: getAssignments() still works and is not going away. This is a better way to ask the same questions, and it removes a class of bug you have probably already hit.


The one-line version

// before
$ass = ConfigboxCacheHelper::getAssignments();
if (!isset($ass['question_to_product'][$questionId])) { /* handle absent */ }
$productId = $ass['question_to_product'][$questionId];

// after
$productId = ConfigboxAssignmentsHelper::getProductIdForQuestion($questionId);
if ($productId === null) { /* handle absent */ }

Every relation the array had now has a method. Nothing in core reads the array any more.


Why bother — three reasons, in order of how likely they are to bite you

1. The undefined-key trap is gone

The assignments map holds only published questions, on published pages of published products. An unpublished or half-created question is legitimately absent, so $ass['question_to_product'][$id] raises an undefined-key warning — and on any install running warnings-as-exceptions (Magento's developer mode does) that is a fatal 500.

That is why the cheat sheet tells you to guard every single read. With the helper there is nothing to guard: a point lookup returns null and a collection returns array(). You still handle the absent case, but forgetting to no longer takes the page down.

2. It does not load the whole catalog to answer one question

getAssignments() builds 24 nested arrays covering every product, page, question, answer, calculation, list and shipping zone in the store, caches them as one entry, and every caller loads all of it. On a dev install that is 24 KB and 0.2 ms, which is why it never looked like a problem. Measured against catalog size:

answers in storecache fileload timememory
6424 KB0.23 ms2 MB
5,000930 KB8.9 ms12 MB
20,0004 MB35 ms44 MB
60,00012 MB119 ms120 MB

Paid per request, in every PHP worker, to answer "which product owns question 4711".

The helper reads what you asked for. It also slice-loads: the first question you ask about pulls that product's whole question↔page↔product map in one query, so the next few hundred lookups for sibling questions cost nothing. A price run over 200 answers is one query, not 200 — and its cost tracks the size of the product, not the store.

3. Nothing for you to invalidate

The helper memoises per request and stops there — no file cache, no APCu. These are single indexed reads against real foreign keys; the caching cost more than it saved. One less thing that can be stale, and one less thing whose cache clear can collide with someone else's.

Within a request the memo invalidates itself. KenedoModel::store(), delete(), copy(), publish() and storeOrdering() drop it whenever the table they wrote is one the relations are derived from, so code that creates a question and then asks what belongs to the product sees the question it just created. publish() matters most: "belongs to" here means "belongs to and the whole chain is published", so publishing changes the answer without touching a single foreign key.

The one case left to you is raw SQL. If you write one of those tables without going through its model, call ConfigboxAssignmentsHelper::forget() afterwards — the same thing forgetRecords() used to be for.


The full mapping

Old array key → new method. id => id means the shape is unchanged, so isset($ids[$x]), array_keys() and foreach keep working exactly as before.

Old keyNew methodReturns
question_to_productgetProductIdForQuestion($questionId)`int
question_to_pagegetPageIdForQuestion($questionId)`int
product_to_questiongetProductQuestionIds($productId)id => id
page_to_questiongetPageQuestionIds($pageId)id => id
product_to_pagegetProductPageIds($productId)id => id
page_to_productgetProductIdForPage($pageId)`int
answer_to_questiongetQuestionIdForAnswer($answerId)`int
answer_to_productgetProductIdForAnswer($answerId)`int
answer_to_pagegetPageIdForAnswer($answerId)`int
question_to_answergetQuestionAnswerIds($questionId)id => id
product_to_answergetProductAnswerIds($productId)id => id
page_to_answergetPageAnswerIds($pageId)id => id
product_to_calculationgetProductCalculationIds($productId)id => id
calculation_to_productgetProductIdForCalculation($calculationId)`int
list_to_productgetListProductIds($listId)id => id
product_to_listgetProductListIds($productId)id => id
country_to_zonegetZoneIdsForCountryId($countryId)id => id
zone_to_countrygetZoneCountryIds($zoneId)id => id
zone_to_shippingmethodgetZoneShippingMethodIds($zoneId)id => id, cheapest first
shippingmethod_to_zonegetZoneIdForShippingMethod($id)`int
city_to_countygetCountyIdForCity($cityId)`int
city_to_stategetStateIdForCity($cityId)`int
city_to_countrygetCountryIdForCity($cityId)`int
county_to_stategetStateIdForCounty($countyId)`int
getCountryIdForCounty($countyId)`int

Legacy element_* aliases (element_to_product, page_to_element, …) are the CB3 spelling of the question_* keys. Migrate to the method for the question relation; there is no element method and there will not be one. See element-question-rename.md.

⚠️ Two shapes deliberately differ

getZoneIdForShippingMethod() returns a scalar, where shippingmethod_to_zone[$id] was a one-element array. zone_id is one column on one row; the array only existed because the map was built by inverting the zone map. If you were looping it, take the value directly.

city_to_country was broken, and the fix adds a key. The old build wrote each county's country into city_to_country keyed by county id. Cities and counties are separate tables with separate auto-increments, so their ids overlap: looking up city 42's country could return the country of county 42 instead — silently, and only for the ids that happened to collide.

getAssignments() now writes those into their own county_to_country map, and city_to_country holds only cities. If your code compensated for the old behaviour, that compensation is now wrong. Use getCountryIdForCity() and getCountryIdForCounty() and delete the workaround.


Migrating your code

The transform is mechanical, and the guards usually disappear with it, because the methods already return a safe empty value:

// guarded ternary -> just the call
$ids = !empty($ass['product_to_question'][$pid]) ? $ass['product_to_question'][$pid] : array();
$ids = ConfigboxAssignmentsHelper::getProductQuestionIds($pid);

// null coalesce -> just the call
$ids = $ass['page_to_question'][$pageId] ?? [];
$ids = ConfigboxAssignmentsHelper::getPageQuestionIds($pageId);

// isset() used as "does this exist" -> compare to null
if (isset($ass['question_to_product'][$qid])) {}
if (ConfigboxAssignmentsHelper::getProductIdForQuestion($qid) !== null) {}

// a "default to 0" read -> keep the 0 explicitly
$productId = !empty($ass['page_to_product'][$pageId]) ? $ass['page_to_product'][$pageId] : 0;
$productId = ConfigboxAssignmentsHelper::getProductIdForPage($pageId) ?: 0;

One trap when you do this by hand. The $ass = getAssignments(); line and the reads that use it are often far apart — a different branch, fifty lines down. Delete the fetch only after grepping the whole function for $ass / $assignments; core's own migration orphaned a reference twice by removing it too eagerly.

Grep pack

grep -rn "getAssignments()" data/customization/
grep -rnE "\$(ass|assignments)\['[a-z_]+_to_[a-z_]+'\]" data/customization/

Clean when both return nothing (or only the deliberate getAssignments() calls you decided to keep).


What has NOT changed

  • getAssignments() itself: same array, same 24 keys, same element_* aliases, still cached. It is part of the customization contract and core still builds it on demand.
  • The published-chain rule. "Belongs to" means "belongs to, and the whole chain is published" — questions and answers require question + page + product published; pages require only their own flag. The helper mirrors this exactly, including that inconsistency. An unpublished question has no answers here, not "its published answers".
  • Ordering. Pages come in configurator order, answers in display order, shipping methods cheapest first — same as the maps they replace.

Core's migration was verified by comparing every method against the live array for every id in the catalog: 865 comparisons, zero differences. If you find one, that is a bug worth reporting.


See also

  • CHEATSHEET.md — the old → new identifier lookup, including the assignment keys
  • element-question-rename.md — the element_*question_* rename
  • technical/com_configbox_kenedo_model.md — models and their property-driven CRUD