Plugin Column Registry
Contract for the runtime seam that merges plugin-registered columns into a core entity's ColumnRegistry.
Overview
App\Sdk\Columns\PluginColumnRegistry holds the PluginColumnDefinition records that add columns to a core entity's list views at runtime.
A core App\Views\ColumnRegistry is a compile-time declaration — its columns() and projectedFields() are code — so without this seam a plugin could not put a column on an entity it does not own. Definitions accumulate here keyed by entity type and column key; each registry merges the definitions addressed to its entityType() while booting, after its declared columns and custom fields. From that point a plugin column is indistinguishable from a core one to every consumer: DataTable rendering, the live-filter column picker, filter and sort validation, and CSV export all read the same merged list.
Column keys are public — saved live filters persist them — so a plugin-scoped registration must carry the vendor prefix, which is what keeps a plugin from shadowing a core column key. A key that nonetheless collides with an existing column is refused when the registry boots rather than silently winning or losing.
Public surface
| Method | Purpose |
|---|---|
register() |
Store one PluginColumnDefinition and drop the memoised registry instance |
has() |
Check whether an entity type already carries a column key |
forEntity() |
Map of column key → definition for one entity type, in registration order |
all() |
Map of entity type → column key → definition |
forPackage() |
Every definition owned by one package |
flush() |
Clear all registrations and memoised registries (tests / reload) |
This seam does not publish a separate registered-value interface: a definition wraps a App\Views\Column, whose documented array shape is the contract. Its current framework contract is the typed public surface on this page plus the documented registration shape and runtime behavior below.
Accepted registration shape
$registry->register(new PluginColumnDefinition(
package: 'acme/fieldops',
entityType: 'rentals',
column: Column::fromConfig([
'key' => 'acme.crew_hours',
'label' => 'Crew Hours',
'field' => 'crew_hours',
'type' => 'string',
'sortable' => true,
'filterable' => true,
'renderer' => 'primary',
'export' => 'crew_hours',
]),
));
Column::fromConfig() owns shape validation: it rejects an unknown or mistyped configuration key, a missing key, and a filter_handler that does not implement ColumnFilterHandler. The registry itself rejects an entity type that no ColumnRegistryResolver mapping knows, and a column key on that entity type already owned by a different package. Re-registering the same key from the same package is idempotent, because plugins re-register on every boot.
Identity is the pair (entity type, column key). Registrations are appended in order, so plugin columns extend the list without reordering the core one.
Runtime consumption
ColumnRegistry::boot()mergesforEntity()after its declared columns, projected schema fields, and custom fields, and throwsInvalidArgumentExceptionnaming the package when a plugin key duplicates an existing column.register()andflush()callColumnRegistryResolver::forget(), because a booted registry memoises its merged column list and would otherwise outlive the registration.- Cost gating, listability, filter handlers, and export paths behave exactly as they do for a core column: a
cost_gatedplugin column is withheld from users withoutcosts.view, and alistable: falseone is filter-only.
Worked example
$registry = app(PluginColumnRegistry::class);
$registry->register(new PluginColumnDefinition(
package: 'acme/fieldops',
entityType: 'rentals',
column: Column::make('acme.crew_hours')->label('Crew Hours')->sortable(),
));
$registry->has('rentals', 'acme.crew_hours'); // true
$registry->forEntity('rentals'); // ['acme.crew_hours' => PluginColumnDefinition]
$registry->forPackage('acme/fieldops'); // every column that package owns
$registry->all(); // ['rentals' => [...]]
app(ColumnRegistryResolver::class)
->resolve('rentals')
->get('acme.crew_hours'); // the merged Column
$registry->flush(); // clears registrations for a reload