Plugin Data Table Registry
Contract for accumulating plugin listing-table definitions registered via PluginRegistrar::datatable().
Overview
App\Sdk\DataTables\PluginDataTableRegistry stores the PluginDataTableDefinition records that back plugin listing tables.
A definition is configuration only: it resolves into mount params for the one canonical components.data-table Livewire component every core listing page already uses, so a plugin table inherits search, sorting, filtering, column toggles, live filters, and CSV export without shipping a view. Definitions are validated at registration time — model class, column config, scope existence, and permission presence — so a malformed declaration fails while the plugin boots rather than when a user opens the page.
Plugins declare tables in signals.yaml (datatables[]) and wire them with PluginRegistrar::datatable($key, $definition). Keys are global, because a datatable page section or a datatableSlot() injection references a table by key alone; manifest validation enforces the vendor prefix. See the DataTable SDK page for the column schema and a live demo.
Public surface
| Method | Purpose |
|---|---|
register() |
Warehouse one PluginDataTableDefinition |
has() |
Check whether a key is registered |
find() |
Return the definition or null |
get() |
Return the definition or throw |
all() |
Map of key → PluginDataTableDefinition |
forPackage() |
Definitions owned by one package |
flush() |
Clear all registrations (tests / reload) |
Accepted registration shape
$registry->register(new PluginDataTableDefinition(
package: 'acme/fieldops',
key: 'acme.fieldops.jobs',
model: \Acme\FieldOps\Models\Job::class,
columns: [
['key' => 'reference', 'label' => 'Job', 'sortable' => true],
['key' => 'status', 'label' => 'Status', 'filterable' => true],
],
permission: 'acme.fieldops.view',
searchable: ['reference', 'title'],
perPage: 24,
emptyMessage: 'No jobs scheduled.',
defaultSort: '-scheduled_at',
with: ['account'],
withCounts: ['tasks'],
scopes: ['open' => true],
));
Identity is the key. Re-registering the same key from the same package is idempotent — plugins re-register on every boot — while a collision across packages throws PluginRegistrarException. get() throws the same exception for an unknown key; find() is the non-throwing form the page and slot renderers use.
A permission is mandatory on every definition: plugin tables are never ungated, whether they target a plugin-owned model or a core App\Models\* one.
Runtime consumption
PluginPageresolves adatatablepage section throughfind(), omits the section when the key is unknown or the viewer lacks the definition's permission, and otherwise mounts the shared component withtoLivewireParams().PluginRegistrar::datatableSlot()reads the definition throughfind()and registers a Livewire slot for the same component, re-checking the definition's permission at render time even when the slot itself is gated on a narrower key.flush()is used when the plugin runtime is rebuilt, so stale definitions never survive a reload.
Worked example
$registry->has('signals.slotsdemo.countries'); // true after the plugin's register()
$definition = $registry->get('signals.slotsdemo.countries');
$params = $definition->toLivewireParams(); // mount props for components.data-table
$registry->forPackage('signals/slots-demo'); // every table that package owns