Skip to content

Slots ​

Introduction ​

A Slot is a contextual value — a reporting date range, a time zone, a locale — that may legitimately arrive from several places. Rather than hard-coding where it comes from, you declare the slot once and let a pipeline resolve it, most specific source first, always ending in a declared default so a slot never resolves to null.

Slots are the third of the three value channels: a #[Parameter] answers which record, a #[Setting] answers how is this configured, and a #[Slot] answers in what context.

The resolution chain ​

For each requested slot, Core walks these sources and takes the first that provides a value:

  1. Request — the query string (?period=quarter). Only the query: a path segment names a record and belongs to #[Parameter], and a request body belongs to the action that receives it.changed in 3.0Route parameters and the request body are no longer read.
  2. Composition — proposals aggregated from the active dashboard's tree (dashboard → groups → tiles/cards), so a dashboard can set a shared context for everything it contains.
  3. Setting — the auto-expanded slot.{name} entry in the settings catalog.
  4. Default — the value you declared on the #[Slot] itself. Always present; the guaranteed fallback.

The SDK adds an Actor source, directly after Request, for the value a person carries — a user's time zone, their reporting period. One value per person and slot; no validity window. In Core alone, the chain is Request → Composition → Setting → Default. See Actor Slot Values.

In a Core-only installation the Setting step and the Default step yield the same value, because the synthetic setting carries the slot's own default and Core has no storage to override it. The chain earns its keep once a storage layer is present: the SDK inserts its Actor source ahead of Composition, and stored per-person values start flowing without a line of your code changing. It does not replace this step — the catalog default stays the floor.

Declaring a slot ​

Slots are declared on the module class (mirroring the #[Role] convention), one #[Slot] per value the module owns. On an artifact class the attribute fails at boot rather than being ignored:

php
use LaravelUi5\Core\Parameters\Attributes\Slot;
use LaravelUi5\Core\Parameters\CoreSlots;
use LaravelUi5\Core\Parameters\Enums\ParameterType;
use LaravelUi5\Core\Parameters\Enums\EditLevel;

enum ReportSlots: string
{
    case Region = 'region';
}

#[Slot(ReportSlots::Region,
    type:     ParameterType::String,
    default:  'EU',
    editable: EditLevel::User,
    note:     'Sales region filter.',
)]
class ReportingModule extends AbstractUi5Module
{
    // …
}
  • Slot identity is an enum case — refactor-safe, and the wire name is the case's ->value ('region'). A plain string is accepted; the enum is recommended.
  • default is mandatory, may not be null, and must already have the slot's type — every slot resolves to something, and that something is typed. A Date/DateTime default may instead be one of the sentinels @today, @now or @first-of-month, resolved at read time; literal values such as 9999-12-31 pass through unchanged.
  • note is mandatory — one short sentence on the slot's purpose (it shows up in ui5:slot <name>). Empty notes fail at boot; 'TBD' is a permitted placeholder.
  • editable (an EditLevel) declares who may change a stored value — a governance hint. Core defines it but does not enforce it (Core is auth-blind) and carries it into the slot's synthetic setting; whether a storage layer checks it is that layer's decision. The conservative default is EditLevel::Administrator, so user-editability is an explicit opt-in.
  • ParameterType::Model is not allowed for slots — slots are wire-shaped values, not record references (that's what #[Parameter] is for).

Validation at boot ​

The registry validates each declaration as it reads it, and every failure is a boot-time LogicException naming the declaring module:

  • the name must match ^[a-z][a-zA-Z0-9_]*$;
  • the note must be non-empty;
  • the type must not be Model;
  • the default must not be null;
  • the default must have the slot's type — 42 or '42' for an Integer, not 'many';
  • a Date/DateTime sentinel must be one Core knows — @tomorow fails here instead of surviving as a literal string;
  • the attribute must sit on a module class — on an artifact it is refused rather than silently ignored.

Two modules may declare the same slot — but only if type, default, note and editable level all agree. Any mismatch is a conflict at boot naming both declarers. That is what makes a slot a shared vocabulary rather than a race.

Consuming slots in an artifact ​

An artifact declares the slots its render depends on via SlottableInterface (getRequiredSlots()); the resolved name → value map is handed to its provider:

php
class SalesReport extends AbstractUi5Report
{
    public function getRequiredSlots(): array
    {
        return [CoreSlots::DateFrom, CoreSlots::DateTo, ReportSlots::Region];
    }
}

// The provider receives the resolved values:
class SalesReportProvider implements DataProviderInterface
{
    public function provide(array $slots): array
    {
        return [
            'from'   => $slots['date_from'],
            'to'     => $slots['date_to'],
            'region' => $slots['region'],
            // …
        ];
    }
}

What a provider receives ​

Each value arrives in the one form its slot's type names, whichever source filled it — a query string, a stored value, a composition proposal or the default:

TypeArrives as
Stringstring
Integerint
Floatfloat
Booleanbool
Decimalnumeric string, e.g. '19.99'
Datestring, Y-m-d, e.g. '2026-09-14'
DateTimestring, ISO-8601 with offset, e.g. '2026-09-14T08:30:00+02:00'

Dates are strings on purpose: slots are wire-shaped and travel in URLs and JSON. Parse where you compute — Carbon::parse($slots['date_from']).

A value without such a form is refused, not skipped:

  • from the request — ?date_from=someday answers 422, naming the slot and the expected form. It does not fall back to the default, which would answer a broken link as if it were fine;
  • from anywhere else — a stored value or a proposal of the wrong type raises InvalidSlotSourceValueException, which is logged, because the fault is in code or data.

Every slot an artifact requests must be in the catalog; an unknown name fails before any source is asked, with an UnknownSlotException — a fault in code, so it is logged and answered with a 500. changed in 3.0Core's wiring faults are logged; until 3.0 they were HTTP exceptions, which Laravel never reports.

An artifact can also propose values to the active composition via getSlotProposals() (a name → value map) — e.g. a dashboard tile that sets the period for the whole dashboard. (See Ui5Dashboard.)

Reading a slot anywhere ​

since 3.0 Where an artifact does not declare its slots — an action handler, a resource

provider, an OData resolver — the context answers for one slot at a time:

php
public function provide(Ui5ContextInterface $context): array
{
    $period = $context->slot(CoreSlots::Period);   // 'month' unless ?period=… says otherwise
    // …
}

slot() is the pipeline's answer for that one name — the same chain as above, so on a dashboard request it agrees with what the dashboard's tiles receive, and with the SDK installed it includes the person's stored value. The pipeline is not a second copy: slot() reads it, it does not extend it.

  • Lazy, never cached. Every call runs the chain. A broken ?period= is the 422 above at the moment you read the slot, not before.
  • Typed — the value arrives in the form of the table above.
  • Named or refused — a name that is not in the catalog throws UnknownSlotException.
  • Without a request — in a command, a job or a test — the query is empty and the chain moves on to composition, setting and default.

A host that binds its own context class through ui5.context_factory implements slot() too; the one-liner is app(ParameterPipelineInterface::class)->resolve([$name])[$name].

Core's canonical slots ​

Core ships six ready-to-use slots — reference them from CoreSlots without re-declaring:

SlotTypeDefaultPurpose
localeStringen-USBCP 47 locale tag
timezoneStringUTCIANA timezone identifier
periodStringmonthPeriod grain: day, week, month, quarter, year
dateDate@todayReporting reference date
date_fromDate@first-of-monthReporting window start (inclusive)
date_toDate9999-12-31Reporting window end (inclusive)

All six are declared EditLevel::User. They are reserved across every LaravelUi5 installation: a module may re-declare them identically, but may not change them.

changed in 3.0The `currency` slot is gone. A base currency is not a per-request value: an installation has one, fixed when it is set up, so a slot that let a URL override it either repeated it or claimed something false.

The settings bridge ​

Every slot additionally auto-expands into a synthetic Setting with the key slot.{name}, owned by Core's own namespace (com.laravelui5.core) and flagged synthetic so tooling can label it as auto-expanded. Type maps from ParameterType to ValueType; default, note and editable level carry through.

Two consequences worth knowing:

  • a slot is addressable through the settings channel without being declared twice — which is what makes step 3 of the resolution chain possible;
  • every settings namespace is a real artifact namespace, with no sentinel pseudo-namespace for slot-derived entries.

The bridge is one-way: a #[Setting] does not become a slot.

Inspecting the catalog ​

One Artisan command introspects every slot the registry has harvested — omit the argument for the catalog, pass a name to drill into one slot:

bash
php artisan ui5:slot           # the whole catalog (name, type, default, note, declarer)
php artisan ui5:slot period    # one slot's catalog entry, and the modules that declare it

From code, the catalog is on the registry:

php
$registry->hasSlot('period');  // bool
$registry->getSlot('period');  // SlotCatalogEntry, throws UnknownSlotException
$registry->getAllSlots();      // array<string, SlotCatalogEntry>

A SlotCatalogEntry carries name, type, default, note, editable, and declaredBy — the module classes that declared it, in registration order.