API Reference
Complete reference for the php-via public API. Methods marked @internal may change between versions.
Via
new Via(Config $config)
$app = new Via(
(new Config())
->withHost('0.0.0.0')
->withPort(3000)
->withTemplateDir(__DIR__ . '/templates')
);
page(string $route, callable $handler): void
Register a page route. Path params use {name} syntax and are injected as typed arguments.
$app->page('/', fn(Context $c) => $c->view('home.html.twig'));
$app->page('/users/{id}', function (Context $c, int $id): void {
$c->view('user.html.twig', ['id' => $id]);
});
broadcast(string $scope): void
Push updates to all contexts in a scope. Use after modifying global state outside an action.
$app->broadcast(Scope::GLOBAL);
$app->broadcast(Scope::routeScope('/'));
$app->broadcast('room:lobby');
globalState(string $key, mixed $default = null): mixed
setGlobalState(string $key, mixed $value): void
Shared in-memory state across all contexts. Persists for the server lifetime; cleared on restart.
$app->setGlobalState('counter', 0);
$count = $app->globalState('counter', 0);
onStart(callable $callback): void
onShutdown(callable $callback): void
Server lifecycle hooks. Use onStart to create timers; onShutdown to clear them.
$timerId = null;
$app->onStart(function () use ($app, &$timerId): void {
$timerId = Timer::tick(5000, fn() => $app->broadcast(Scope::GLOBAL));
});
$app->onShutdown(function () use (&$timerId): void {
if ($timerId !== null) Timer::clear($timerId);
});
onClientConnect(callable $callback): void
onClientDisconnect(callable $callback): void
Fire when a client opens or closes an SSE connection. The callback receives the client's Context.
On disconnect, the client has already been removed from getClients().
See Lifecycle for usage.
$app->onClientConnect(fn(Context $c) => $app->broadcast(Scope::routeScope($c->getRoute())));
$app->onClientDisconnect(fn(Context $c) => $app->broadcast(Scope::routeScope($c->getRoute())));
getClients(): array
$clients = $app->getClients();
// ['contextId' => ['id' => '...', 'ip' => '...', 'connected_at' => 1234567890]]
$count = count($app->getClients());
appendToHead(string ...$elements): void
$c->appendToHead(
'<title>My Page</title>',
'<meta name="description" content="...">'
);
getTwig(): Twig\Environment
$app->getTwig()->addExtension(new MyExtension());
log(string $level, string $message, ?Context $context = null): void
$app->log('info', 'Server ready');
$app->log('debug', 'Action fired', $c);
start(): void
Start the OpenSwoole server. Call this last, after all routes are registered.
---Context
One context per connected browser tab. Holds signals, actions, and view definition for that client.
signal(mixed $initial, ?string $name, ?string $scope, bool $autoBroadcast, bool $clientWritable)
$count = $c->signal(0); // TAB scope, auto-named
$count = $c->signal(0, 'count'); // named
$shared = $c->signal(0, 'count', Scope::ROUTE); // shared across route
$pref = $c->signal('light', 'theme', Scope::SESSION);
$room = $c->signal('', 'note', 'room:lobby'); // custom scope
// Collaborative: allow clients to write to a scoped signal
$input = $c->signal('', 'note', Scope::ROUTE, clientWritable: true);
Scoped signals are server-authoritative by default — clients cannot overwrite
shared state. Use clientWritable: true for collaborative inputs where multiple
clients need to push values (e.g. shared text fields with data-bind).
action(callable $fn, ?string $name = null): Action
$inc = $c->action(function (Context $c) use ($count): void {
$count->setValue($count->int() + 1);
$c->sync();
}, 'increment');
// In Twig: data-on:click="@post('/_action/increment')"
view(callable|string $view, array $data = [], ?string $block = null, bool $cacheUpdates = true): void
$c->view('counter.html.twig', ['count_val' => $count->int()]);
$c->view(fn() => "<p>Count: {$count->int()}</p>");
// Send only {% block demo %} on SSE updates (full template on initial load):
$c->view(fn() => $c->render('todo.html.twig', $data), block: 'demo');
// Disable caching for views that read external state:
$c->view('presence.html.twig', ['n' => count($app->getClients())], cacheUpdates: false);
component(callable $fn, string $namespace): callable
Create a reusable sub-context. See Components.
$counter = $c->component(function (Context $c): void {
$n = $c->signal(0, 'n');
$inc = $c->action(fn() => $n->setValue($n->int() + 1), 'inc');
$c->view('counter.html.twig', ['n_val' => $n->int(), 'inc_url' => $inc->url()]);
}, 'counter');
$c->view('page.html.twig', ['counter' => $counter()]);
scope(string $scope): void
addScope(string $scope): void
$c->scope(Scope::ROUTE); // set primary scope
$c->addScope('room:' . $roomId); // add additional scope
sync(): void
syncSignals(): void
sync() re-renders the view and pushes signals. syncSignals() pushes only signals, skipping view re-render.
broadcast(): void
Broadcast to all contexts in this context's primary scope.
execScript(string $script): void
$c->execScript('document.querySelector(".flash").classList.add("show")');
setInterval(callable $callback, int $ms): int
Recurring timer, auto-cancelled when the context is destroyed.
$c->setInterval(function () use ($count, $c): void {
$count->setValue($count->int() + 1);
$c->sync();
}, 1000);
onDisconnect(callable $callback): void
onCleanup(callable $callback): void
Per-context disconnect hook for cleaning up resources. onCleanup is an alias. See Lifecycle.
$c->onDisconnect(function (Context $c) use ($room, $app): void {
unset(Room::$members[$room][$c->getId()]);
$app->broadcast('room:' . $room);
});
Getters
$c->getId(); // unique context ID
$c->getRoute(); // current route, e.g. '/chat/lobby'
$c->getSessionId(); // session ID shared across tabs
$c->getPathParam('id'); // path parameter by name
$c->getNamespace(); // component namespace, or null
---
Signal
// Reading
$s->getValue(); // raw mixed
$s->int(); // cast to int
$s->string(); // cast to string
$s->bool(); // cast to bool
// Writing
$s->setValue(42); // triggers broadcast if scoped
// Template helpers
$s->id(); // signal name, e.g. 'count'
$s->bind(); // 'data-bind="count"'
$s->text(); // '<span data-text="$count"></span>'
---
Action
$a->url(); // '/_action/increment'
$a->id(); // 'increment'
---
Scope
Scope::TAB // default — per browser tab
Scope::ROUTE // all clients on same URL
Scope::SESSION // all tabs of same user
Scope::GLOBAL // every connected client
Scope::routeScope('/path') // explicit: 'route:/path'
---
Config
All methods return self for chaining.
(new Config())
->withHost('0.0.0.0') // default: '0.0.0.0'
->withPort(3000) // default: 3000
->withDevMode(true) // enables debug logging
->withLogLevel('info') // 'debug' | 'info' | 'error'
->withTemplateDir(__DIR__ . '/templates')
->withShellTemplate(__DIR__ . '/shell.html')
->withStaticDir(__DIR__ . '/public')
->withBasePath('/') // URL base path prefix
->withSsePollIntervalMs(50) // SSE poll interval in ms
->withSwooleSettings([...]) // raw OpenSwoole server settings