# Column API

> Column options, built-in value formats, inference, and transformation contracts.

Source: https://docs.inertiax.dev/table/reference/column/

Column classes live in `InertiaX\Table\Columns`. Create a builder with `ColumnClass::make(string $key, ?string $label = null)`.

## Common options

| Method                                              | Effect                                                                                                                                 |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `key(string $key)`                                  | Set the source field key                                                                                                               |
| `label(?string $label)`                             | Set the heading; an omitted label is derived from the key                                                                              |
| `relationship(string $relation, string $attribute)` | Read a persisted scalar field through a single Eloquent `BelongsTo` relation                                                           |
| `type(ColumnType\|string $type)`                    | Set a built-in type or namespaced custom type                                                                                          |
| `sortable(bool $value = true)`                      | Permit sorting                                                                                                                         |
| `searchable(bool $value = true)`                    | Include in global search                                                                                                               |
| `filterable(bool $value = true)`                    | Permit automatic filter generation                                                                                                     |
| `copyable(bool $value = true)`                      | Show the copy control                                                                                                                  |
| `toggleable(bool $value = true)`                    | Include in the column picker                                                                                                           |
| `visible(bool $value = true)`                       | Set initial visibility                                                                                                                 |
| `wrap(bool $value = true)`                          | Wrap displayed content                                                                                                                 |
| `truncate(?int $length)`                            | Limit displayed text length; `0` disables truncation; `null` or `clearTruncate()` removes the builder override                         |
| `filter($filter, ?string $label = null)`            | Attach a filter object, builder, Filter class name, or key string; it queries this column and keeps its own key, or takes the column’s |
| `meta(array\|string $meta, mixed $value = null)`    | Merge column metadata or set one metadata key                                                                                          |

`ColumnType` lives in `InertiaX\Table\Columns\Enums`. For defaults, see [Defaults](https://docs.inertiax.dev/table/reference/defaults/#column-settings). Clearing a truncation override restores inherited/default behavior; use `truncate(0)` to disable truncation even when the table sets a length. Hidden values are still sent to the browser; see [Security](https://docs.inertiax.dev/table/guide/security/#visibility-is-presentation).

## Built-in value formats

| Type             | Options and defaults                                                                                                                       |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `TextColumn`     | Text value                                                                                                                                 |
| `NumberColumn`   | `numberCast(string $cast)` defaults to `float`; accepts `int`, `integer`, `real`, `float`, `double`, `decimal`                             |
| `BooleanColumn`  | `trueLabel(string)` / `falseLabel(string)` default to `True` / `False`; `trueIcon(string)` / `falseIcon(string)` set icons                 |
| `DateColumn`     | `format(string)` defaults to `Y-m-d`                                                                                                       |
| `DateTimeColumn` | `format(string)` defaults to `Y-m-d H:i:s`                                                                                                 |
| `TimeColumn`     | `format(string)` defaults to `H:i:s`                                                                                                       |
| `JsonColumn`     | Preserves structured JSON; React formats its display                                                                                       |
| `BadgeColumn`    | `variant(Closure\|array\|string\|Variant $variant, mixed $default = null)` and `icon(Closure\|array\|string $icon, mixed $default = null)` |

Date, datetime, and time columns also support `timezone(?string)` and `translate(bool = true)`. The default timezone is the application timezone; translation is off by default and uses the application locale when enabled. Formatting does not mutate a model-owned Carbon value.

Integer casts preserve large integers as decimal strings; `decimal` preserves a string value. Invalid numeric values fail instead of becoming zero. Boolean labels and icons affect presentation; the cell value remains Boolean.

The built-in renderer styles `primary`, `secondary`, `success`, `danger`, `warning`, and `info` badge variants. These are also the cases in `InertiaX\Table\Columns\Enums\Variant`. Other variant strings are accepted but need application styling or a custom renderer.

## Number presentation

```php
NumberColumn::make('amount_cents', 'Amount')
    ->numberCast('int')
    ->currency('USD', divideBy: 100)
    ->locale('en-US');

NumberColumn::make('ratio')->numberCast('decimal')->decimals(3)->locale('fr-FR');
```

| Method                                          | Accepted values and behavior                                                                                                                 |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `currency(string $currency, int $divideBy = 1)` | Three uppercase letters, such as `USD` or `JPY`; `divideBy` must be a power of ten from `1` through `1000000000`                             |
| `decimals(int $digits)`                         | Exactly `0` through `20` fractional digits                                                                                                   |
| `locale(string $locale)`                        | Two- or three-letter language, optional four-letter script, optional two-letter or three-digit region: `en`, `en-US`, `zh-Hans-CN`, `es-419` |

Calling any presentation method enables formatting. Without `currency()`, the style is decimal; locale defaults to `en-US` and the divisor to `1`. Without `decimals()`, currency precision follows `Intl.NumberFormat` currency defaults; decimal formatting uses its default precision. Currency codes are syntax-checked, not checked against a bundled currency registry. Locale matching is case-insensitive; variants and extensions are unsupported. The renderer canonicalizes the locale and rejects locales unavailable in its runtime instead of falling back to an unrelated locale.

Formatting changes the built-in cell text, title and copy text together. It leaves the number cast, raw cell value and query units unchanged. Integer/decimal strings are scaled exactly before formatting; an earlier float conversion can already have lost precision. Null displays as empty; negative zero keeps its sign. Non-numeric transformer or decorator output fails with table and column context. Per-row currencies, exchange conversion and locale-aware filter input need application code.

A NumberColumn subclass can declare protected nullable `$currency`, `$locale`, `$divideBy` and `$fractionDigits` properties. Their defaults are `null` (unconfigured); inherited values apply until a fluent method overrides that option. `currency()` overrides both currency and divisor. Formatting alone enables no query capability. Label a filter for scaled cents **Amount (cents)**, since `8419` is still its input value for `$84.19`.

For stored cents, use `->numberCast('int')->currency('USD', divideBy: 100)` without a transformer that also divides by 100; combining them scales the amount twice. Use a transformer when you intend to change the serialized value itself.

`@inertiax/table-core` exports `formatTableNumber(value, format)` for custom renderers; `NumberFormat` belongs to `@inertiax/table-protocol`. To reuse one `Intl.NumberFormat`, call `createTableNumberFormatter(format)` and retain its returned function within a component or request. Bound any multi-format cache. Invalid format options and values throw; formatting does not negotiate the browser locale.

## Related scalar fields

```php
TextColumn::make('customer_name', 'Customer')
    ->relationship('customer', 'name')
    ->searchable()
    ->sortable()
    ->filterable();
```

Use `relationship()` with an Eloquent query source when a column represents one persisted field on a related model. The example uses the order model’s `customer()` relation and the customer’s `name` field. The column key stays `customer_name` in table state. Enable search, sorting and filtering separately; the binding alone does not enable them.

The displayed value and built-in comparisons use the same related database field, with the relation’s keys, ordinary `where` constraints and model scopes. That includes the comparisons of a filter attached with `filter()` and of a filter whose `column()` names this column. Missing or scoped-out related records produce `null`. PHP accessors, casts and `withDefault()` objects on the related model do not change that value. Column transformers still receive the root record; use a loaded relation and `transformUsing()` when you need accessor or default-object presentation.

Only standard, single-hop `BelongsTo` relations are supported. Custom foreign and owner keys and same-table relations work. The table’s row key must remain a root field. Related fields must be persisted scalars: JSON paths, computed properties and selected aliases require explicit queries or callbacks. Ordinary root joins and explicit root projections are supported when the root table keeps its original name and the selected fields include the row key. Root scopes must preserve added projections: use `addSelect()` instead of replacing the selection with `select()`.

The helper rejects collection/custom sources, aliased root sources, cross-connection relations, other relation families, and relation queries with ordering, limits, offsets, grouping, having, unions, joins, custom selects, distinct or locks. Same-table relations additionally require simple column constraints; raw SQL, nested condition groups and subqueries need an explicit query. These declaration errors fail before retrieving table rows.

`sortUsing()` and `searchUsing()` retain their explicit overrides. Custom clause `applyUsing()` and filter `applyClauseUsing()` callbacks receive the column key as `$column` and own their query behavior; the helper does not rewrite custom SQL. Existing dotted keys such as `customer.name` retain their data-path behavior and do not declare a query binding. For caller-owned projections, see [custom search and sorting](https://docs.inertiax.dev/table/reference/column/#query-callbacks).

## Relationship counts and sums

```php
NumberColumn::make('orders_count', 'Orders')
    ->count('orders')
    ->numberCast('int')
    ->sortable()
    ->filterable();

NumberColumn::make('paid_total', 'Paid total (cents)')
    ->sum('paidOrders', 'amount_cents')
    ->sortable()
    ->filterable();
```

Use `count()` or `sum()` with an Eloquent query source and a standard, single-hop `HasMany` relation. Here, `paidOrders()` can be an ordinary named relation such as `return $this->orders()->where('paid', true);`. Custom relation keys, ordinary `where` constraints and related model scopes apply to both the displayed aggregate and built-in comparisons. Each parent keeps one row and its original row key.

An empty count is `0`. A sum is `null` when there are no matching rows or all matching values are null; it is not converted to zero. `NumberColumn` casts to `float` by default; the count example chooses `int`. Sum fields must be persisted numeric database fields. Related PHP accessors and casts do not change the aggregate.

The methods declare a value; enable sorting and filtering separately. Numeric aggregates do not participate in default global text search. Sorting uses the aggregate, then the root row key for ties; null placement follows the database. Built-in filters compare stored units: an `amount_cents` sum of `300` matches an `equals` filter value of `300`, even when currency presentation displays `$3.00`. Use `->numberCast('int')->currency('USD', divideBy: 100)` for stored cents; null sums still display as empty.

The [query restrictions above](https://docs.inertiax.dev/table/reference/column/#related-scalar-fields) also apply to aggregates, including the restrictions on same-table constraints and on relation ordering, limits and custom selects. Nested, polymorphic, through and pivot relations need explicit queries. `sortUsing()`, custom clause callbacks and `applyClauseUsing()` callbacks retain ownership of their queries. Existing `withCount()` / `withSum()` projections remain usable through those callbacks; a key such as `orders_count` alone never declares an aggregate or enables automatic comparison of a selected alias. The last `relationship()`, `count()` or `sum()` call on a builder selects its binding.

## Automatic columns

`autoColumns()` shows the fields the model serializes with `toArray()`:

1. the attributes each row carries: every column of the model’s table, or what a query selects, including selects that global scopes add and aliases such as `withCount()` counts; for a collection, the attributes its first model loaded;
2. then the model’s `$appends`;
3. limited to `$visible` when it is set, in the order `$visible` lists;
4. without `$hidden` fields and the fields passed to `except`.

It always adds the row key, as a hidden column that is not toggleable or filterable, even when `$visible` leaves it out or `$hidden` or `except` names it. Appended accessors cannot be sorted, searched, or filtered, and neither can a query’s aliases or columns joined from another table. A raw select contributes only its final `as` alias, and another table’s `*` adds no field. When no field remains besides the row key, `autoColumns()` throws a `ColumnDefinitionException`. Explicit columns with the same key take precedence and appear after the inferred columns. Explicit `AutoColumn::make($key)` infers only that field and retains its chained options.

`autoColumns(except: ['internal_notes'])` leaves fields out of one table; in a Table class, set `$defaultAutoColumnsExcept`. `autoColumns()` without `except` keeps the list already set.

Inference needs an Eloquent model from the query or first collection row. Recognized model casts win over schema types. The `array` cast infers `JsonColumn` with both `autoColumns()` and `AutoColumn::make()`, preserving structured values, empty arrays, and nulls. An unrecognized cast falls back to text; without a cast, schema inference runs, followed by timestamp-name heuristics (`_at`), then text. Schema inference reads the database type, so a `bigint unsigned` column becomes a number column. On MySQL and SQLite, Laravel’s `boolean()` creates a `tinyint(1)` column, which becomes a boolean column and accepts only 0 and 1. On SQL Server it creates `bit`, which also becomes a boolean column. A MySQL or PostgreSQL `bit(n)` column stays text. Schema inspection failures are logged and use fallback inference. A query that selects `*` needs schema inspection to list the table’s columns: if inspection fails, `$visible` takes their place, and `autoColumns()` throws when `$visible` is empty.

Use explicit columns for parameterized or custom casts whose desired type is not inferred, and for plain array rows or empty collections. Review [field exposure](https://docs.inertiax.dev/table/guide/security/#choose-fields-deliberately) before using bulk inference.

## Value transformers

`transformUsing($transformer)` appends a transformer; repeated calls do not replace earlier stages. `mapAs(array $map, mixed $default = null)` adds a mapping at priority 10.

| Method                      | Parameters                                                  |
| --------------------------- | ----------------------------------------------------------- |
| `addValueTransformer()`     | `$transformer, ?int $priority = null, ?string $name = null` |
| `replaceValueTransformer()` | `string $name, $transformer`                                |
| `removeValueTransformer()`  | `string $name`                                              |
| `clearValueTransformers()`  | None; also removes built-in transformations                 |

Transformers accept a closure, a `ValueTransformer` object, or its container-resolved class name. The contract’s `transform()` receives the value, record, column, and table. Returning `null` produces a null cell value. Transformations affect serialized cells, not query comparisons.

Lower priorities run first; the default priority is 100. Equal priorities retain declaration order. Replacing a named stage retains its priority and position. Duplicate names and missing replacement/removal targets fail.

## Cell decorators

`decorateUsing($decorator)` appends a decorator. Parallel methods are `addCellDecorator($decorator, ?int $priority = null, ?string $name = null)`, `replaceCellDecorator(string $name, $decorator)`, `removeCellDecorator(string $name)`, and `clearCellDecorators()`.

A closure or container-resolved `CellDecorator` receives the current `CellPayload`, record, column, and table. It may mutate or replace the payload; returning `null` retains the current payload. Ordering follows the same rules as transformers. Both contracts live in `InertiaX\Table\Columns\Contracts`.

Column `meta()` applies to every row; decorator metadata belongs to one cell. See [Custom columns](https://docs.inertiax.dev/table/guide/custom-columns/) for an example.

## Query callbacks

For Eloquent and collections, `sortUsing(?Closure $callback, DataSourceKind|string ...$sources)` and `searchUsing(?Closure $callback, DataSourceKind|string ...$sources)` customize query behavior. Searchable columns whose `type` is not `text` require `searchUsing()`, including a `TextColumn` with a custom type. `supportsSortSources(...$sources)` and `supportsSearchSources(...$sources)` set the supported kinds separately. `DataSourceKind` is in `InertiaX\Table\Enums`. See [Custom query behavior](https://docs.inertiax.dev/table/guide/query-callbacks/) for arguments and return contracts.

Custom sources reject these callbacks. See [Custom-source search](https://docs.inertiax.dev/table/reference/source/#search) for their searchable-column requirements.

See [Callback context](https://docs.inertiax.dev/table/reference/callbacks/) for injectable arguments and resolution rules.
