Skip to main content
Version: 3.x

Join

Version
3.x
Updated
View markdown

An integer foreign key, with the referenced record's display value joined in so lists and forms can show something a human recognises.

Storage kind: column · Column: int unsigned

Use it whenever a field points at exactly one row of another model. Use multiselect when several may be picked, pseudojoin when the options come from a model method and the stored value is a string, and calculation for a calculation reference specifically — it is a join subclass with the extras that case needs.

Settings

SettingMeaning
modelClassThe model being referenced
modelMethodHow to fetch its records — usually getRecords
propNameKeyThe referenced property holding the key. Usually id
propNameDisplayThe referenced property shown to the user
defaultlabelPlaceholder for the picker
defaultPre-selected value on a new record
groupbyGroup the options by another property's value
dropdownOrderingOrder of the option list
lockedAfterStoreOnce set, render read-only (value kept in a hidden input)
parentMarks a parent relationship — makes selects and joins recurse, see below
joinAdditionalPropsPull further properties of the referenced model into the record
filterparentsOffer the referenced model's list filters on this list too
isPseudoJoinSuppress the SQL join — see below

What reads produce

Besides the key itself, a read exposes <name>_display_value carrying propNameDisplay's value from the referenced record. That is what list cells and the picker's current label use, and it is why a join costs nothing extra to display.

joinAdditionalProps

When the display value is not enough, name further properties of the referenced model to pull into the record:

'joinAdditionalProps' => array(
array('propertyName' => 'sku'),
array('propertyName' => 'price', 'selectAliasOverride' => 'product_price'),
),

Without selectAliasOverride the key is prefixed joinedby_<property>_to_<parentModel>_<property> — verbose but collision-proof. With it you choose the key. A named property that does not exist on the referenced model is logged and skipped, not thrown — so a typo here shows up as a silently missing key, not an error.

parent

'parent' => 1 says the referenced model is this record's parent, and makes select and join building recurse into that model's own joins — so a page's record can carry its product's fields, and the product's joins in turn. It is what makes a hierarchy readable in one query. Recursion follows parent only, so a non-parent join stops at one level and stays cheap.

filterparents is the list-view counterpart: the referenced model's filters are offered above this list as well, so a page list can be filtered by product.

isPseudoJoin — two unrelated uses

This flag means "do not emit the SQL join", and it is set for two different reasons that must not be confused:

  1. An alias collision. Two joins onto the same parent model would alias to the same table name. party.owner_agent_party_id and party.default_billing_address_id are genuine integer FKs that suppress the join for this reason.
  2. A picker with no table behind it — options come from a model method, and the stored value is a string key. That case is now its own type: pseudojoin.

If you are choosing between them: a picker keyed by an integer id is a join; one keyed by a string is a pseudojoin. Use 1 only when you genuinely have two joins to one model and have to break the collision.

Notes

  • defaultlabel is the picker's placeholder, not a "none" option. On a required reference it reads like a prompt ("Select Configurator Page"). Optionality comes from required, not from the presence of this label — reading it as a "none" choice produced a field described as optional when it was not.
  • propNameDisplay must exist on the referenced model, or the property throws on read — deliberately, since the alternative is a silently empty column in every list. propNameKey gets the same treatment, with a message naming both models.
  • lockedAfterStore is for a reference that must not move once the record exists — reparenting would orphan its children. The value still posts, from a hidden input, so the save round-trips.
  • storeExternally composes correctly here: getJoinsForGetRecord() calls the parent first, so an externally stored join emits both the external-table join and the referenced-model join.

@see pseudojoin.md · calculation.md · multiselect.md