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): 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.

Next steps

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