mbolli / php-via
Real-time engine for building reactive web applications in PHP
v0.5.0
2026-03-25 19:42 UTC
Requires
- php: ^8.4
- ext-openswoole: *
- nyholm/psr7: ^1.8
- psr/http-server-middleware: ^1.0
- starfederation/datastar-php: dev-main
- twig/twig: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.90.0
- openswoole/ide-helper: ^22.1
- pestphp/pest: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1.0
- phpstan/phpstan-deprecation-rules: ^2.0
- rector/type-perfect: ^2.1
- tomasvotruba/type-coverage: ^2.0
README
Real-time reactive web framework for PHP. Server-side reactive UIs with zero JavaScript, using OpenSwoole for async PHP, Datastar (RC.8) for SSE + DOM morphing, and Twig for templating.
Why php-via?
- No JavaScript to write — Datastar handles client-side reactivity, SSE, and DOM morphing
- Twig templates — familiar, powerful server-side templating
- No build step — no transpilation, no bundling, no node_modules
- Real-time by default — every page gets a live SSE connection
- Scoped state — TAB, ROUTE, SESSION, GLOBAL, and custom scopes control who shares what
- Single SSE stream — extremely efficient with Brotli compression
Requirements
- PHP 8.4+
- OpenSwoole extension
- Composer
Installation
composer require mbolli/php-via
Quick Start
<?php require 'vendor/autoload.php'; use Mbolli\PhpVia\Via; use Mbolli\PhpVia\Config; use Mbolli\PhpVia\Context; $config = new Config(); $config->withTemplateDir(__DIR__ . '/templates'); $app = new Via($config); $app->page('/', function (Context $c): void { $count = $c->signal(0, 'count'); $step = $c->signal(1, 'step'); $increment = $c->action(function () use ($count, $step, $c): void { $count->setValue($count->int() + $step->int()); $c->syncSignals(); }, 'increment'); $c->view('counter.html.twig', [ 'count' => $count, 'step' => $step, 'increment' => $increment, ]); }); $app->start();
counter.html.twig:
<div id="counter"> <p>Count: <span data-text="${{ count.id }}">{{ count.int }}</span></p> <label>Step: <input type="number" data-bind="{{ step.id }}"></label> <button data-on:click="@post('{{ increment.url }}')">Increment</button> </div>
php app.php
# → http://localhost:3000
Core Concepts
Full documentation at via.zweiundeins.gmbh/docs
Signals — reactive state that syncs between server and client
$name = $c->signal('Alice', 'name'); $name->string(); // read $name->setValue('Bob'); // write → auto-pushes to browser
<input data-bind="{{ name.id }}"> <span data-text="${{ name.id }}">{{ name.string }}</span>
Actions — server-side functions triggered by client events
$save = $c->action(function () use ($c): void { $c->sync(); }, 'save');
<button data-on:click="@post('{{ save.url }}')">Save</button>
Scopes — control who shares state and receives broadcasts
| Scope | Sharing | Use Case |
|---|---|---|
Scope::TAB |
Isolated per tab (default) | Personal forms, settings |
Scope::ROUTE |
All users on same route | Shared boards, multiplayer |
Scope::SESSION |
All tabs in same session | Cross-tab state |
Scope::GLOBAL |
All users everywhere | Notifications, announcements |
Custom ("room:lobby") |
All contexts in that scope | Chat rooms, game lobbies |
Views — Twig template files or inline strings
$c->view('dashboard.html.twig', ['user' => $user]);
Path Parameters — auto-injected by name
$app->page('/blog/{year}/{slug}', function (Context $c, string $year, string $slug): void { // ... });
Components — reusable sub-contexts with isolated state
$a = $c->component($counterWidget, 'a'); $b = $c->component($counterWidget, 'b');
Lifecycle Hooks
$c->onDisconnect(fn() => /* cleanup */); $c->setInterval(fn() => $c->sync(), 2000); // auto-cleaned on disconnect $app->onClientConnect(fn(string $id) => /* ... */);
Broadcasting — push updates to other connected clients
$c->broadcast(); // same scope $app->broadcast(Scope::GLOBAL); // all contexts $app->broadcast('room:lobby'); // custom scope
How it Works
1. Browser requests page → Server renders HTML, opens SSE stream
2. User clicks button → Datastar POSTs signal values + action ID
3. Server executes action → Modifies signals / state
4. Server pushes patches → HTML fragments + signal updates via SSE
5. Datastar morphs DOM → UI updates without page reload
Development
git clone https://github.com/mbolli/php-via.git
cd php-via && composer install
cd website && php app.php # run website + examples on :3000
vendor/bin/pest # 101 tests, 258 assertions
composer phpstan # PHPStan level 6
composer cs-fix # code style
Deployment
Single OpenSwoole process behind a reverse proxy. See deploy/ for systemd + Caddy configs.
Browser → Caddy (TLS + Brotli) → OpenSwoole :3000
Roadmap
- Route groups (
$app->group('/prefix', fn)) -
initAtBoot()— explicit hook for boot-time shared state initialisation - Global intervals (
$app->setInterval()— one shared timer per server process)
Credits
- Datastar — SSE + DOM morphing
- OpenSwoole — Async PHP
- Twig — Templating
- go-via/via — Original Go inspiration
License
MIT
