Fields¶
A field maps one entity property to a column in lists, a value on the
record page and an input in forms. Text::make('sku', 'SKU') names the
property and, optionally, the label; without one the label is derived
from the name.
Catalog¶
The descriptor names each field by its type, the identifier in the second column.
Class |
Type |
Holds |
|---|---|---|
|
|
The identifier. Read-only, sortable, never on a form. |
|
|
String columns; |
|
|
Long text, edited in wide inputs. A JSON value is a string column. |
|
|
Formatted content — headings, lists, links, tables, images — stored as a structured document; see below. |
|
|
Markdown text for technical content, edited visually or as source; |
|
|
Numbers; |
|
|
An amount in the currency’s minor units (cents, whole yen); see Money and currencies. |
|
|
An ISO 4217 code in a string column, chosen from a searchable list that names each code in the UI’s language ( |
|
|
True or false. |
|
|
A calendar day, |
|
|
A backed enum: |
|
|
A JSON object of text values in a string column, edited as key/value rows; |
|
|
An uploaded file’s storage key; see Files and images. |
|
|
A read-only value a method of the resource computes from the record; see below. |
|
|
Relations to another resource, made with |
Common modifiers¶
Every field takes:
Modifier |
Effect |
|---|---|
|
Its label, and a hint under the input. |
|
Validation, checked by the server and mirrored by the form. Any framework constraint may be used. |
|
The value a new record starts with: the create form shows it pre-filled, and a create that leaves the field out takes it, validated like a value that was sent. Action dialogs and settings pages start from defaults too. |
|
Shown, never written. |
|
Allowed in the list grammar. |
|
Where the field appears. |
|
A totals row under the list. |
|
Editable in its list cell; see Forms. |
|
The value is unique (as its column’s index enforces; the ORM’s metadata does not carry indexes, so the field says so): a duplicated record starts without it, and an import can match rows by it. The descriptor publishes |
|
Rendered by a custom cell or input; see Customizing the UI. |
|
Extra settings passed to the UI. |
Conditions (visibleWhen(), requiredWhen(), disabledWhen()),
dependent choices and repeaters are on Forms.
Passwords¶
A Password field stores what is written, so the resource’s create()
or apply() hashes it — or the field is hashed(): the value is then
hashed through the application’s
Kinetis\Vector\Auth\PasswordHasherInterface, bound on AppScope in
bootstrap.php, or through the implementation hashed(OtherHasher::class)
names, for a table hashed differently. When a field hashes through the
interface and nothing binds it, the first request refuses with the
reason rather than storing plain text. An empty value on a form keeps
the stored hash.
Duplicating¶
A record’s page offers Duplicate when the user may create and the
resource is replicable() (true unless it returns false): a create form
filled from the record. It copies every value the create form writes — a
many-to-many’s membership, a repeater’s rows as new rows — except a
field marked unique(), which its column’s unique index would refuse,
and file or image keys, since a stored key belongs to its record. The
copy is saved through the ordinary create. The descriptor publishes
replicable on the resource.
Filter operators¶
A filterable field offers the operators its value admits:
Value |
Operators |
|---|---|
Text |
|
Number, money, id |
|
Boolean |
|
Date, datetime |
|
Enum, belongs-to |
|
A nullable column adds null and nnull. Collections are never
filterable. The list grammar and “any of” groups are on REST API.
Rich text¶
RichText gives authors a word-processor-like editor (Tiptap, on
ProseMirror) and stores what they write as the editor’s JSON
document — a tree, not markup — in a string property on a TEXT or
JSON column. The server validates every write against a fixed schema,
so sanitizing becomes schema validation:
RichText::make('description')
->features(['bold', 'italic', 'heading', 'list', 'link', 'table']) // default: all but image and mention
->imageField('imageKey') // images upload through this file/image field and store its key
->mentions('assignee') // @ offers the records of this relation
->textField('descriptionText'), // search reads the plain text the writer fills here
Features are what the editor offers and the server accepts:
bold,italic,underline,strike,code,heading,list,blockquote,codeBlock,horizontalRule,link,table,image,mention— all but images and mentions by default.Validation. The value must be valid JSON with a
{"type": "doc", "content": [...]}root; every node and mark type must be one of the field’s features, and every attribute one that type defines; a link goes only tohttp(s):,mailto:or a relative place; the document is at mostmaxBytes()(256 KiB by default), 32 levels deep and 20 000 nodes. A refusal is a422on the field, with the path into the document (description/content/2/marks/0/attrs/href). An empty document is no value, sorequired()works.Images store a storage key, never a URL: one the image field’s upload endpoint issued, or one already in the stored document. The admin fetches it with its credentials through
GET /vector/r/{resource}/{id}/files/{imageField}?key=…, which serves only keys a document of that record shows.Search needs
textField('bodyText'): a second string property the writer fills withDocument::toPlainText()on every write. The build refusessearchable()without one. A rich-text field is never sorted or summarized, and filtered only bynullandnnull.
On your own pages¶
Kinetis\Vector\RichText\Document renders a stored value without
knowing the editor:
public function __construct(private Document $document) {}
$html = $this->document->toHtml($product['description'], Document::DEFAULT_FEATURES,
static fn (string $key): ?string => "/media/{$key}"); // or null to leave an image out
toHtml() renders through
ueberdosis/tiptap-php,
configured with exactly the given features — never its highlighting code
blocks, which reach the filesystem or a process. What they do not cover
is dropped, and text and attributes are escaped. toPlainText() is the
projection CSV exports and search use. The demo’s shop page,
GET /shop/products/{id}, renders the product description this way.
An application node type, such as a callout or an embed, is registered on both sides; see Customizing the UI.
HTML storage¶
RichText::make('body')->storesHtml() is an opt-in for an existing HTML
column: the admin still sends the document, and the server stores
toHtml() of it. The admin reads the stored HTML back through the same
schema, so anything outside it is dropped on the next save. Fidelity is
only what HTML carries — prefer the document for new columns. The demo’s
ticket body is one.
Markdown¶
Markdown is for technical text. The stored value is the Markdown
source; the admin edits it visually through @tiptap/markdown, with a
view of the source, and shows it through markdown-it (HTML in the source
off, bare URLs linked) and the same sanitizer. The descriptor publishes
richtext: {features, imageField, mentions, storesHtml, maxBytes} and
markdown: {toolbar}.
In the browser¶
The admin renders HTML only from a document it generated itself through
the field’s schema (or from markdown-it with HTML off), then passes it
through DOMPurify with an allow-list matching that schema: no style,
no event handlers, no src. That happens in the layer’s only v-html,
in Vx/Cell/RichText.vue and Vx/Cell/Markdown.vue. A sanitizer that
fails its canary check is not trusted, and values are then shown as
text. DOMPurify is dual-licensed MPL-2.0 OR Apache-2.0; the layer uses
it under Apache-2.0. Under the hosted shell’s strict CSP the editor’s
own style element carries the response nonce, table resizing (which
writes style attributes) is off, and images display as data: URLs:
the editor and the record page run without a single violation.
Computed fields¶
A computed field shows a value your resource calculates, such as a line total or a full name, on the list and the record page:
Computed::make('lineTotal', 'Total')->as('money')->currencyField('order.currency')
->from(['quantity', 'unitPriceCents'])->method('lineTotal'),
/** @param array<string, mixed> $line the serialized record */
public function lineTotal(array $line): ?int
{
return $line['quantity'] * $line['unitPriceCents'];
}
as()— how it is shown:text,number(decimals()),money(currency()orcurrencyField()),boolean,date,datetime, orenumwithoptions(). The value is typed by it and exported like any field.from()— the fields the method reads: every list and record that shows the computed value carries them too, whatever thecolumnsasked for.method()— a public instance method of the resource, built by the request container and called once per record with the record already serialized. It runs no query of its own, so a list’s statement count does not change.
A computed field is read-only, and never sorted, filtered, searched,
summarized, written or imported. The build refuses one that asks for
any of those, one without a method, one whose method is not a public
instance method, an enum without options, and a source that is not a
field holding a value. If a user may not see a field it reads
(viewField()), they do not see the computed field either, so a
derived value never shows what the user may not see. The descriptor
publishes computed: {as, from}; the method’s name stays on the server.
Relations¶
BelongsToshows the target’s title and edits with a searchable picker.titleField()overrides which field names the target.HasManyandManyToManyshow a count in lists and a panel on the record page. A many-to-many is edited in the form, or attached and detached from the panel.A relation never crosses databases; a value pointing at another database is an ordinary field. See Databases.
Money and currencies¶
Money is stored in minor units, following the currency’s ISO 4217
exponent: hundredths for most currencies, whole units for JPY,
thousandths for BHD. The schema publishes the exceptions to hundredths
as minorUnits, so the UI types and formats each amount correctly. The
input keeps what is typed and writes two decimals only when the field
is left.
The currency comes from one of three places:
currency('EUR'), fixed;currencyField('currency'), a field of the same record, serialized with every view that shows the money field;currencyField('order.currency'), a field of a related record reached through a belongs-to. That relation then carries the value as{id, label, currency}, and inside a repeater a row reads the parent being edited.
A filter on a money field priced per record reads the typed amount in
hundredths. VECTOR_CURRENCY sets the starting code of a Currency
field that declares no default() (and allows the code).
See also¶
Forms — conditions, dependent choices, repeaters, inline edits.
Files and images — uploads.
Authorization — hiding fields per user.