Documentation menu

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

Next steps

  • Signals — reactive state in depth
  • Actions — server-side event handlers
  • Scopes — sharing state across clients
  • Lifecycle — connect, disconnect, timers