Signals
Signals are php-via's reactive state primitive. They hold a value, and when that value changes, every browser subscribed to the context sees the update via SSE.
Creating a signal
$count = $c->signal(0); // anonymous signal
$step = $c->signal(1, 'step'); // named signal (better Datastar binding)
The first argument is the initial value. Signals can hold any JSON-serializable value: integers, strings, booleans, arrays.
Reading and writing
$count->int(); // read as int
$count->string(); // read as string
$count->getValue(); // raw value
$count->setValue(42); // update — queues a patch to every subscribed client
Binding in templates
In a Twig template, use Datastar attributes to bind signals to the DOM:
<!-- Display value (one-way, signal named 'count') -->
<span data-text="${count}"></span>
<!-- Two-way binding for input (signal named 'step') -->
<input data-bind="step" type="number">
<!-- Conditional visibility -->
<div data-show="$visible">...</div>
<!-- Use signal.id() when the name is auto-generated -->
<span data-text="{{ count.id() }}"></span>
Scopes & sharing
By default, signals are TAB-scoped: each visitor has their own independent copy. You can change the scope to share state across visitors:
use Mbolli\PhpVia\Scope;
// Only this browser tab sees this
$private = $c->signal(0, 'count');
// Everyone on this URL sees the same value
$shared = $c->signal(0, 'count', Scope::routeScope('/'));
$c->scope(Scope::ROUTE); // equivalent shorthand
Open another browser tab with this page open, then increment both counters. The TAB counter is yours alone; the ROUTE counter is shared.
Syncing
When signals change inside an action, the framework automatically queues patches for connected clients.
If you update signals outside an action (e.g., in a timer callback), call $c->sync() or $app->broadcast($scope):
$c->setInterval(function () use ($count, $c): void {
$count->setValue($count->int() + 1);
$c->sync(); // push the update
}, 1000);