Join
- Version
- 4.0 preview
- Updated
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
| Setting | Meaning |
|---|---|
modelClass | The model being referenced |
modelMethod | How to fetch its records — usually getRecords |
propNameKey | The referenced property holding the key. Usually id |
propNameDisplay | The referenced property shown to the user |
defaultlabel | Placeholder for the picker |
default | Pre-selected value on a new record |
groupby | Group the options by another property's value |
dropdownOrdering | Order of the option list |
lockedAfterStore | Once set, render read-only (value kept in a hidden input) |
parent | Marks a parent relationship — makes selects and joins recurse, see below |
joinAdditionalProps | Pull further properties of the referenced model into the record |
filterparents | Offer the referenced model's list filters on this list too |
isPseudoJoin | Suppress 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:
- An alias collision. Two joins onto the same parent model would alias to the same table name.
party.owner_agent_party_idandparty.default_billing_address_idare genuine integer FKs that suppress the join for this reason. - 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
defaultlabelis the picker's placeholder, not a "none" option. On a required reference it reads like a prompt ("Select Configurator Page"). Optionality comes fromrequired, not from the presence of this label — reading it as a "none" choice produced a field described as optional when it was not.propNameDisplaymust exist on the referenced model, or the property throws on read — deliberately, since the alternative is a silently empty column in every list.propNameKeygets the same treatment, with a message naming both models.lockedAfterStoreis 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.storeExternallycomposes 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