Lifecycle
php-via provides hooks at every stage of a context's life — from server start to client disconnect. Use them to manage timers, presence tracking, and resource cleanup.
Connection lifecycle
Here's what happens for each client:
- Page load (GET) — page handler runs, initial HTML rendered
- SSE connect (GET /_sse) —
onClientConnectfires, context syncs - Actions — POST to
/_action/{name}, callback runs, patches stream back - SSE disconnect —
onClientDisconnectfires immediately - Cleanup (after timeout) —
onDisconnect/onCleanupfire, timers cancelled
If the browser reconnects within the cleanup timeout (default: 5 seconds), the context is reused and cleanup is cancelled — you won't see a spurious disconnect/reconnect for normal page refreshes.
onClientConnect (app-level)
Register an app-level callback that fires whenever any client's SSE connection is established.
The callback receives the connecting client's Context:
$app->onClientConnect(function (Context $c) use ($app): void {
// Re-render the route to update presence counters for everyone there
$app->broadcast(Scope::routeScope($c->getRoute()));
});
onClientDisconnect (app-level)
Fires when an SSE connection closes. The client has already been removed from getClients()
when this fires, so any presence count reflects the departure:
$app->onClientDisconnect(function (Context $c) use ($app): void {
$app->broadcast(Scope::routeScope($c->getRoute()));
});
These two hooks together replace a polling timer for presence features. See Broadcasting.
onDisconnect / onCleanup (per-context)
Register cleanup logic per context. Fires after the SSE connection has been closed and the cleanup timeout has elapsed (or the browser sent a close beacon). Use this for resource cleanup specific to that user's session:
$c->onDisconnect(function (Context $c) use ($room, $app): void {
unset(Room::$members[$room][$c->getId()]);
$app->broadcast('room:' . $room);
});
// onCleanup() is an alias:
$c->onCleanup(function (Context $c): void {
// same thing
});
Recurring timers
$c->setInterval() creates a OpenSwoole timer scoped to the context.
It fires repeatedly at the given interval (ms) and is automatically cancelled when the context is cleaned up:
$c->setInterval(function () use ($elapsed, $c): void {
$elapsed->setValue($elapsed->int() + 1);
$c->sync();
}, 1000); // every 1 second
No need to store the timer ID or cancel it manually — it cleans up with the context.
Server-level timers
For timers that run regardless of who is connected (e.g. a stock price tick), use
onStart with an OpenSwoole Timer::tick(), and clean up in onShutdown:
$timerId = null;
$app->onStart(function () use ($app, &$timerId): void {
$timerId = Timer::tick(5000, function () use ($app): void {
$price = fetchStockPrice();
$app->setGlobalState('price', $price);
$app->broadcast(Scope::routeScope('/ticker'));
});
});
$app->onShutdown(function () use (&$timerId): void {
if ($timerId !== null) {
Timer::clear($timerId);
}
});
Next steps
- Broadcasting — pushing updates to multiple clients
- Components — component-level lifecycle hooks
- API: onDisconnect() — method signature
- API: onClientConnect() — app-level hook signature