# Pseudojoin

> A picker whose options come from a model method rather than a table, storing the option's own string key: a template folder name, a PSP connector name, a lan…

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

---
A picker whose options come from a model **method** rather than a table, storing the option's own
**string** key: a template folder name, a PSP connector name, a language tag.

**Storage kind:** `column` · **Column:** `varchar(100)`

Use it when the thing being chosen is not a database row — templates on disk, installed connectors,
registered providers. If the options *are* rows and the value is their id, you want
[`join`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/join); if the set is a short fixed list known in code, you want
[`dropdown`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/dropdown).

## Settings

Declared like a [`join`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/join), because it is one — the class extends `KenedoPropertyJoin`:

| Setting | Meaning |
|---|---|
| `modelClass` | The model exposing the option list |
| `modelMethod` | The method returning it — e.g. `getProductTemplates`, `getPspConnectors` |
| `propNameKey` | The key field in each option, usually `value` |
| `propNameDisplay` | The label field, usually `title` |
| `defaultlabel` | Placeholder |

```php
$propDefs['template'] = array(
    'name'            => 'template',
    'type'            => 'pseudojoin',
    'modelClass'      => 'ConfigboxModelTemplates',
    'modelMethod'     => 'getProductTemplates',
    'propNameKey'     => 'value',
    'propNameDisplay' => 'title',
);
```

Because there is no table, there is no SQL join and no `<name>_display_value` from one: the label
comes from the option list itself, re-fetched when the form renders.

## Why it exists

These were declared as `'type' => 'join', 'isPseudoJoin' => true`. That worked — `KenedoPropertyJoin`
checks the flag and skips the SQL join — but everything *describing* the property saw "join" and
inferred an `int unsigned` foreign key, when the column really holds a short string. The generated
schemas, the type descriptions and the schema-drift check were all wrong in the same way.

The class extends `KenedoPropertyJoin` and forces `isPseudoJoin` in its **constructor**, before the
parent sees the definition — `getPropertyDefinition()` is `final`, so there is no other interception
point. Query building is therefore unchanged; what changes is that the type now states what it is,
and `getDefaultDataType()` can answer `varchar(100)`.

`varchar(100)` comfortably holds a template folder or connector name — the longest in use is seven
characters — while staying narrow enough to index. A picker whose keys are genuinely longer carries
an explicit `dataType`.

## Choosing between this and `join`

- Options come from a **table**, value is an **integer id** → [`join`](https://docs.configbox.at/docs/4.0-preview/technical/property-types/join)
- Options come from a **method**, value is a **string key** → `pseudojoin`

`isPseudoJoin` on a `join` is still honoured, so customization code declaring the old shape keeps
working. It is also still correct for its other use: a genuine integer FK suppressing the join to
avoid an alias collision between two joins onto the same parent model.

## Notes

It has its **own template** (`tmpl/pseudojoin.php`) that delegates to the join template. That file
has to exist: templates resolve by type name, and without one the field renders as nothing — the
form shows no control, nothing is submitted, and the insert fails on a NOT NULL column far from the
cause. That is exactly how this type's first version broke product-list saves.

The same trap applies to any type you add: see §6 of the overview.

@see join.md
@see dropdown.md
