SIGNALS Documentation
API Reference

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

  • PluginPage resolves a datatable page section through find(), omits the section when the key is unknown or the viewer lacks the definition's permission, and otherwise mounts the shared component with toLivewireParams().
  • PluginRegistrar::datatableSlot() reads the definition through find() 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