Slot Registry
Contract for accumulating plugin UI slot injections and resolving them by slot key, priority, and permission.
Overview
App\Sdk\Slots\SlotRegistry is the in-memory registry of plugin UI slot injections. Registrations accumulate at plugin boot and are rendered by App\Sdk\Slots\SlotRenderer.
Plugins never ship Blade views. Each registration points at a core Blade/Livewire component name or a trusted HTML callable, plus optional context data.
Navigation is not a rendered slot — plugins already register nav items through the registrar nav seam and must not duplicate that path here.
Public surface
| Method | Purpose |
|---|---|
register() |
Append one SlotDefinition |
for() |
Return definitions for a slot key, priority ascending |
all() |
Return every accumulated definition in append order |
flush() |
Clear all definitions |
Accepted definition shape
Each entry is an immutable App\Sdk\Slots\SlotDefinition:
new SlotDefinition(
package: 'signals/xero-sync',
slot: 'dashboard.widgets',
type: SlotComponentType::View,
component: 'signals.xero-sync.widget',
data: fn (array $context): array => ['title' => 'Xero'],
priority: 50,
permission: null,
)
App\Sdk\Slots\SlotComponentType cases:
| Case | Value | Meaning |
|---|---|---|
View |
view |
Core Blade component name + data callable → props array |
Html |
html |
Callable returning a trusted HTML string |
Livewire |
livewire |
Core Livewire component name + params callable |
priority defaults to 50. When permission is set, SlotRenderer skips the component unless Auth::user()?->can($permission).
There is no keyed identity and no duplicate rejection. Multiple packages may inject into the same slot; register() always appends.
Starter slot keys
Wired into core views today:
rental.detail.header_actionsrental.detail.tabsaccount.detail.header_actionsinvoice.detail.header_actionsdashboard.widgets
Ordering and lookup behavior
for($slot) filters by exact slot key, then sorts by priority ascending (lower first). Equal priorities retain relative order from the filtered list after usort.
all() returns the raw append-order list. flush() empties the registry (used between boots / tests).
Runtime consumption and renderer isolation
SlotRenderer::render($slot, $context) is the runtime consumer:
- It walks
for($slot)in priority order - Permission-gated definitions are skipped when the current user cannot pass
can($permission) - Each component renders inside a per-definition
try/catch - A throwing component is logged and skipped so siblings and the host page continue
The registry itself never renders markup or catches exceptions.
Host views mount slots with:
<x-signals.plugin-slot name="dashboard.widgets" :context="[]" />
Worked example
This is the current accumulation and lookup shape:
$registry->register(new SlotDefinition(
package: 'signals/xero-sync',
slot: 'dashboard.widgets',
type: SlotComponentType::Html,
component: 'inline',
data: fn (array $context): string => '<div>Xero status</div>',
priority: 10,
));
$registry->for('dashboard.widgets'); // priority-ascending list for that slot
SlotRenderer::render('dashboard.widgets', []) walks that list, applies permission checks, and concatenates successfully rendered HTML chunks under failure isolation.