Documentation menu

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:

  1. Page load (GET) — page handler runs, initial HTML rendered
  2. SSE connect (GET /_sse)onClientConnect fires, context syncs
  3. Actions — POST to /_action/{name}, callback runs, patches stream back
  4. SSE disconnectonClientDisconnect fires immediately
  5. Cleanup (after timeout)onDisconnect / onCleanup fire, 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