Skip to content

arqel-dev/widgets — API Reference

Namespace Arqel\Widgets\. Dashboard widget system: KPI cards (Stat), charts (Chart), mini-tables (Table), and an escape-hatch for arbitrary React components (Custom). Supports polling, deferred loading, per-user visibility, and declarative filters shared across a dashboard's widgets.

Arqel\Widgets\Widget (abstract)

Base class for all widget types. Constructor: (string $name).

MethodTypeDescription
heading(string) / description(string)staticVisual chrome
sort(int)staticOrdering within the dashboard grid
columnSpan(int|string)static1..12, or a shorthand string ('full', '1/2')
poll(int $seconds)staticClient refetch interval; 0 or negative disables polling
deferred(bool = true)staticWhen true, toArray() emits data: null and the client fetches lazily via WidgetDataController
canSee(Closure)staticReceives ?Authenticatable, returns bool
filters(array<string, mixed>)staticSeeds/overrides the widget's filter map
data()array (abstract)Per-render payload — subclasses implement
getName() / getType() / getComponent() / getHeading() / getDescription() / getSort() / getColumnSpan() / getPollingInterval() / isDeferred()getters
filterValue(string $name, mixed $default = null)mixedCanonical filter reader inside data()
getFilters()array<string, mixed>
id()stringDefault <type>:<name>
canBeSeenBy(?Authenticatable $user)booltrue when no canSee set
toArray(?Authenticatable $user = null)arrayInertia payload: { id, name, type, component, heading, description, sort, columnSpan, poll, deferred, filters, data }

Subclasses declare protected string $type (snake_case) and protected string $component (PascalCase React component name).

StatWidget (not final — subclassed by arqel:widget --type=stat)

KPI card. Factory StatWidget::make($name).

MethodTypeDescription
value(mixed)selfScalar or Closure(): scalar, resolved at data() time
statDescription(mixed)selfSecondary line (e.g. '+12% vs last week'); string or Closure
descriptionIcon(string) / icon(string)self
color(string)selfOne of primary|secondary|success|warning|danger|info (COLOR_* constants); unknown falls back to primary
chart(mixed)selfarray<int|float> or Closure — sparkline points
url(string)selfRenders the card as a link when set

ChartWidget (not final — subclassed by arqel:widget --type=chart)

Serializes Recharts config; rendering happens client-side. Factory ChartWidget::make($name).

MethodTypeDescription
chartType(string)selfOne of line|bar|area|pie|donut|radar (CHART_* constants)
height(int)selfPixels, min 50
showLegend(bool = true) / showGrid(bool = true)self
chartData(array|Closure)self{ labels, datasets: [{label, data, color}, ...] }
chartOptions(array|Closure)self
getChartType() / getHeight() / isLegendVisible() / isGridVisible()getters

TableWidget (not final — subclassed by arqel:widget --type=table)

Mini-table; intentionally has no hard dependency on arqel-dev/table (columns are duck-typed via toArray()). Factory TableWidget::make($name).

MethodTypeDescription
query(Closure(): Builder)selfMust return an Eloquent Builder (or builder-shaped object)
limit(int)selfMin 1, default 10
columns(array)selfObjects exposing toArray(); others dropped silently
seeAllUrl(string|Closure|null)self

Errors thrown inside the query Closure are caught and surfaced as loadError on the payload rather than crashing the dashboard.

CustomWidget (final)

Escape-hatch for arbitrary React components — composed via make(), never subclassed.

MethodTypeDescription
CustomWidget::make(string $name, string $component)selfFactory
component(string)selfThrows InvalidArgumentException on empty string
withData(array|Closure)selfPayload emitted to the React component; note the setter is withData(), not data() (PHP's LSP rules prevent narrowing data(): array's return type)

Arqel\Widgets\Dashboard (final)

Declarative dashboard schema: a list of widgets + shared layout. Factory Dashboard::make(string $id, string $label, ?string $path = null).

MethodTypeDescription
widgets(array<Widget|class-string<Widget>>)selfClass-strings resolved through the container at resolve() time; invalid entries dropped silently
addWidget(Widget|class-string<Widget>)self
columns(int|array<string, int>)selfFlat 1..12, or a responsive map keyed by sm|md|lg|xl|2xl, each clamped 1..12
heading(string) / description(string)self
filters(array)selfDual-mode: legacy array<string, mixed> passthrough, or list<Filter> declarative (detected by presence of any Filter instance)
canSee(Closure)selfReceives ?Authenticatable
canBeSeenBy(?Authenticatable $user)bool
getWidgets() / getColumns() / getHeading() / getDescription() / getFilters() / getDeclaredFilters() / getFilterDefaults()getters
resolve(?Authenticatable $user = null)arrayResolves class-string widgets, filters by canBeSeenBy, sorts by getSort(), merges dashboard filter defaults into each widget, returns { id, label, path, widgets, filters, columns, heading, description }
toArray(?Authenticatable $user = null)arrayAlias for resolve()
findWidget(string $widgetId)?WidgetLookup by id(); authorization intentionally not enforced here (callers distinguish 404 vs 403)

Arqel\Widgets\DashboardRegistry (final, singleton)

register(Dashboard) (throws InvalidArgumentException on duplicate id — no silent overwrite), has(id), get(id): ?Dashboard, all(): array<string, Dashboard>, clear().

Arqel\Widgets\WidgetRegistry (final, singleton)

register(string $type, class-string<Widget> $widgetClass) (validates is_subclass_of(Widget::class)), has(type), get(type): ?class-string<Widget>, all(), clear().

Filters

Arqel\Widgets\Filters\Filter (abstract)

Declarative dashboard-level filter, propagated into each widget's filter map at resolve() time. Constructor (string $name) is final; factory Filter::make($name).

MethodTypeDescription
label(string)staticDefault = Str::of($name)->snake()->replace('_',' ')->title()
default(mixed)static
getName() / getLabel() / getDefault() / getType() / getComponent()getters
getResolvedDefault()mixedThe serializable default (same value toArray()['default'] emits) — use this, not getDefault(), when seeding a filter map
toArray()array{ name, type, component, label, default, ...typeSpecificProps }

Filters\DateRangeFilter (final)

type='date_range', component='DateRangeFilter'. defaultRange(?DateTimeInterface $from, ?DateTimeInterface $to): static stores the range; resolveDefault() serializes both endpoints as Y-m-d strings ({from, to}) so a raw DateTimeInterface never leaks its cast shape to the client.

Filters\SelectFilter (final)

type='select', component='SelectFilter'. options(array|Closure) (Closure resolved lazily at toArray() time), multiple(bool = true).

HTTP

Registered in routes/admin.php under web + auth + HandleArqelInertiaRequests (needed for the sidebar/panel.navigation shared prop):

VerbRouteNameController
GET/adminarqel.dashboard.mainHttp\Controllers\DashboardController::show
GET/admin/dashboards/{dashboardId}arqel.dashboard.showHttp\Controllers\DashboardController::show
GET/admin/dashboards/{dashboardId}/widgets/{widgetId}/dataarqel.dashboard.widget-dataHttp\Controllers\WidgetDataController::show

Both controllers abort_unless(Dashboard::canBeSeenBy($user), 403) before resolving further; WidgetDataController additionally checks Widget::canBeSeenBy() and seeds dashboard filter defaults under request-supplied filter values before calling data().

Artisan commands

CommandFunction
arqel:widget <Name> --type=stat|chart|table|custom --forceScaffolds app/Widgets/<Name>.php. stat/chart/table generate a final subclass; custom generates a final factory composing CustomWidget::make()
arqel:dashboard <Name> --id=<custom> --forceScaffolds app/Dashboards/<Name>.php with a static make(): Dashboard factory

Example

php
use Arqel\Widgets\StatWidget;

final class TotalUsersWidget extends StatWidget
{
    public function __construct()
    {
        parent::__construct('total_users');
        $this->heading('Total users')->columnSpan(3)->poll(60);
    }

    public function data(): array
    {
        return ['value' => User::count()];
    }
}
php
use Arqel\Widgets\Dashboard;
use Arqel\Widgets\Filters\{DateRangeFilter, SelectFilter};

return Dashboard::make('main', 'Overview')
    ->columns(['default' => 1, 'md' => 2, 'lg' => 4])
    ->filters([
        DateRangeFilter::make('period'),
        SelectFilter::make('status')->options(['active' => 'Active', 'archived' => 'Archived']),
    ])
    ->widgets([
        TotalUsersWidget::class,
        RevenueChartWidget::class,
    ]);

MIT License — built with Inertia + React + Laravel.