SIGNALS Documentation
API Reference

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_actions
  • rental.detail.tabs
  • account.detail.header_actions
  • invoice.detail.header_actions
  • dashboard.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:

  1. It walks for($slot) in priority order
  2. Permission-gated definitions are skipped when the current user cannot pass can($permission)
  3. Each component renders inside a per-definition try / catch
  4. 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.