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): RouteDefinition
Register a page route. Path params use {name} syntax and are injected as typed arguments. Returns a RouteDefinition for fluent middleware registration.
$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]);
});
// With per-route middleware:
$app->page('/admin', fn(Context $c) => $c->view('admin.html.twig'))
->middleware(new AuthMiddleware());
middleware(MiddlewareInterface ...$middleware): void
Register global PSR-15 middleware that runs on every page and action request. See Middleware.
$app->middleware(new CorsMiddleware([...]));
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);
});
setInterval(callable $callback, int $ms): void
Register a process-wide recurring timer that fires every $ms milliseconds.
The timer is started automatically when the server starts and cleared on shutdown — no manual
onStart/onShutdown wiring required.
// Broadcast global state every 5 seconds
$app->setInterval(fn() => $app->broadcast(Scope::GLOBAL), 5000);
group(callable $fn): RouteGroup
Register multiple routes inside a closure and attach shared middleware to all of them in one call.
Returns a RouteGroup with a fluent middleware() method.
Only routes registered inside the closure are included.
use Mbolli\PhpVia\Http\RouteGroup;
$app->group(function (Via $app): void {
$app->page('/admin', fn(Context $c) => ...);
$app->page('/admin/users', fn(Context $c) => ...);
})->middleware(new AuthMiddleware());
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
Signals and actions registered on the context are auto-injected into every Twig render
as named variables (signal baseName, action camelCase name). Entries in $data take priority.
// count and increment are auto-injected — no explicit passing needed
$c->view('counter.html.twig');
// Only pass data that isn't a signal/action:
$c->view('todo.html.twig', ['todos' => $todos]);
$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', ['todos' => $todos]), 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');
// n and inc are auto-injected into counter.html.twig
$c->view('counter.html.twig');
}, 'counter');
$c->view('page.html.twig', ['counter' => $counter()]);
getSignal(string $name): ?Signal
getAction(string $name): ?Action
Retrieve a registered signal or action by name after registration. Useful in actions that need to look up signals by name rather than via a captured variable.
$c->getSignal('count'); // returns the Signal registered as 'count', or null
$c->getAction('increment'); // returns the Action registered as 'increment', or null
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);
});
cookie(string $name): ?string
setCookie(string $name, string $value, int $expires = 0, string $path = '/', bool $httpOnly = true, bool $secure = true, string $sameSite = 'Lax'): void
deleteCookie(string $name, string $path = '/'): void
Coroutine-safe cookie access. cookie() reads a request cookie (replaces $_COOKIE, which is unsafe in OpenSwoole). setCookie() queues a cookie for the response with secure defaults. deleteCookie() expires a cookie immediately. Cookies queued during a page load are flushed by RequestHandler; cookies set inside an action are flushed by ActionHandler.
$theme = $c->cookie('theme'); // null if not set
$c->setCookie('theme', 'dark'); // Secure, HttpOnly, SameSite=Lax by default
$c->deleteCookie('theme'); // expires cookie immediately
file(string $name): ?array
Read a file from a multipart form upload. Returns the upload array (name, type, tmp_name, size) or null if the field is missing or errored. Use Datastar's contentType: 'form' modifier to submit the form as a real multipart POST.
$upload = $c->file('avatar');
if ($upload !== null) {
move_uploaded_file($upload['tmp_name'], '/uploads/' . basename($upload['name']));
}
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
sessionData(string $key, mixed $default = null): mixed
setSessionData(string $key, mixed $value): void
clearSessionData(?string $key = null): void
Per-session server-side storage, keyed on the session cookie. Survives page refreshes and context destruction — unlike signals, which reset on refresh.
$c->setSessionData('auth', ['user' => 'ada', 'role' => 'Engineer']);
$auth = $c->sessionData('auth'); // returns the array or null
$c->clearSessionData('auth'); // clear one key
$c->clearSessionData(); // clear entire session bucket
input(string $name, mixed $default = null): mixed
Read a query or POST parameter safely. Checks POST first, then query string. Replaces direct $_GET/$_POST access (which is coroutine-unsafe in OpenSwoole).
$id = $c->input('id'); // POST['id'] ?? GET['id'] ?? null
$cat = $c->input('cat', 'all'); // with default
getRequestAttribute(string $name, mixed $default = null): mixed
Read a request attribute set by PSR-15 middleware. See Middleware.
$user = $c->getRequestAttribute('user'); // set by AuthMiddleware
---
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('/') // base path when mounted at a sub-path (see below)
->withSsePollIntervalMs(50) // SSE poll interval in ms
->withSwooleSettings([...]) // raw OpenSwoole server settings
// Security
->withSecureCookie(true) // __Host- prefix, Secure flag
->withTrustedOrigins(['https://example.com']) // CSRF origin allowlist
->withActionRateLimit(100, 60) // max 100 actions per 60s per IP
// HTTPS / HTTP2 / Brotli compression (requires ext-brotli)
->withCertificate('/path/to/cert.pem', '/path/to/key.pem') // direct TLS + HTTP/2
->withH2c() // h2c (cleartext HTTP/2) for proxy scenarios
->withBrotli() // enable Brotli; withBrotli(dynamicLevel: 4, staticLevel: 11)
// Multi-worker (single machine)
->withWorkerNum(swoole_cpu_num()) // default: 1; requires SwooleBroker/RedisBroker/NatsBroker when > 1
->withGlobalStateTableSize(2048, 8192) // GlobalState table: rows × value bytes (default 1024 × 4096)
// Multi-node broker (optional — required only for multi-worker / multi-server deployments)
->withBroker(new SwooleBroker()) // same machine, no external deps
->withBroker(new RedisBroker('127.0.0.1', 6379)) // or new NatsBroker(...)
->onBrokerError(fn(\Throwable $e) => error_log($e->getMessage())) // called on reconnect
withBrotli() requires either withCertificate() (direct HTTPS) or withH2c()
(proxy with Caddy/Nginx handling TLS). A hard error is thrown at start() if ext-brotli
is missing or neither HTTPS mode is configured.
See Deployment → Brotli for setup guides.
withBasePath(string $basePath) sets the URL prefix when the app is mounted at a sub-path
(e.g. example.com/myapp/). The value must be a relative path such as /,
/myapp, or /sub/path. An \InvalidArgumentException is thrown
at startup for invalid inputs (absolute URLs, protocol-relative paths, etc.).
The basePath is exposed to every Twig template as /.
See Deployment → Sub-path mounting for a Caddy example.