# Join

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

Source: CBX documentation, version 4.0 preview (unreleased). Canonical page: https://docs.configbox.at/docs/4.0-preview/technical/property-types/join. Last updated 2026-07-27.

---
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`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/multiselect) when several may be picked, [`pseudojoin`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/pseudojoin) when the
options come from a model method and the stored value is a string, and
[`calculation`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/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:

```php
'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`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/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
